Scrollable Nav Bar

Unit Converter in Python Tkinter : Temperature, Length, and Weight

A Unit Converter is a perfect GUI project because it looks like a real desktop tool while still being beginner-friendly. In one project you practice: Tkinter layout, dropdowns, input validation, event handling, and clean conversion logic.

In this tutorial, readers will build the full project by copying code snippets step-by-step. Every snippet is explained in detail, including:

  • how this code snippet connects to the previous snippet
  • how it connects to the next snippet
  • which part of the code belongs inside the same class that started earlier (so the class flow stays clear)

Important rule while building: In Python, indentation defines scope. Anything indented under class UnitConverterApp(tk.Tk): belongs to the class (constants + methods). Anything indented under a method like def __init__(self): belongs inside that method.


Project Overview

You will build a desktop app called Unit Converter with:

  • Category dropdown: Temperature / Length / Weight
  • From Unit and To Unit dropdowns (auto-updated per category)
  • Value input field
  • Buttons: Convert, Swap, Clear, Exit
  • Output area showing original + converted values (with unit symbols)

Requirements

  • Python 3.x
  • Built-in libraries only: tkinter, tkinter.ttk, tkinter.messagebox

Folder Structure

unit-converter/
  unit_converter.py

How it connects: You’ll paste every snippet into the same file (unit_converter.py) in the exact order shown below. Each next snippet assumes the previous one is already present.


How to Run the Project

Run normally (recommended)

python unit_converter.pyCode language: CSS (css)

If you installed Anaconda

  • Open Anaconda Prompt
  • Go to your folder and run:
cd path/to/unit-converter
python unit_converter.py

If you want to try Jupyter Notebook

You can paste the final built file into a notebook cell, but Tkinter may behave differently on some setups. If the GUI doesn’t open, run the .py file from Anaconda Prompt/terminal.


Build the Code Step-by-Step

Step 1: Imports (Top of the File)

Create a file named unit_converter.py and start with these imports:

import tkinter as tk
from tkinter import ttk, messagebox
Code language: JavaScript (javascript)

This is the first code in the file, so there is nothing before it. tkinter gives you the main GUI toolkit, ttk provides modern themed widgets like Combobox, and messagebox is used for pop-up error dialogs. The next step will start your main application class, and these imports must be available before that class can use tk.Tk, ttk.Label, ttk.Button, and messagebox.showerror().


Step 2: Start the App Class + Add Absolute-Zero Constants

Paste this directly below the imports:

class UnitConverterApp(tk.Tk):
    """A super-beginner friendly Unit Converter GUI using Tkinter."""

    # Absolute zero constants
    ABS_ZERO_C = -273.15
    ABS_ZERO_F = -459.67
    ABS_ZERO_K = 0.0

This line starts the class UnitConverterApp, which means everything indented under it belongs to the same class. The docstring explains what the class represents. The three constants are stored at the class level (not inside a method) so any method can access them using self.ABS_ZERO_C and similar. This snippet connects forward to the __init__ method next, because __init__ is where the window, data, and UI are created when you run the app.


Step 3: Add the __init__ Method Header + Window Setup

Paste the following inside the class (notice the indentation):

    def __init__(self):
        super().__init__()

        # Window setup
        self.title("Unit Converter")
        self.geometry("520x320")
        self.minsize(520, 320)
Code language: PHP (php)

Because this snippet is indented under the class, it becomes a method of UnitConverterApp. super().__init__() creates the actual Tkinter window (the base tk.Tk object). The title and geometry define how the window looks when it launches, while minsize prevents the UI from becoming cramped. The next snippet will continue inside this same __init__ method to define which units the app supports.


Step 4: Add the Units Dataset (Still Inside __init__)

Continue directly after the previous snippet (same indentation level inside __init__):

        # Supported units by category
        self.units_by_category = {
            "Temperature": ["Celsius (°C)", "Fahrenheit (°F)", "Kelvin (K)"],
            "Length": [
                "Meters (m)",
                "Kilometers (km)",
                "Centimeters (cm)",
                "Millimeters (mm)",
                "Inches (in)",
                "Feet (ft)",
            ],
            "Weight": ["Grams (g)", "Kilograms (kg)", "Pounds (lb)", "Ounces (oz)"],
        }
Code language: PHP (php)

This dictionary is the “source of truth” for dropdown values. When the user chooses a category like Length, the app will pull the matching list of units from this dictionary to populate the From/To dropdowns. This connects backward because the window already exists and now needs data to display. It connects forward because the next snippet creates StringVar() variables that store the user’s chosen category, units, and input value.


Step 5: Add Tkinter Variables (Still Inside __init__)

Paste next, continuing inside __init__:

        # Tkinter variables
        self.category_var = tk.StringVar(value="Temperature")
        self.from_unit_var = tk.StringVar()
        self.to_unit_var = tk.StringVar()
        self.value_var = tk.StringVar()
Code language: PHP (php)

These StringVar() objects connect your widgets to Python variables. For example, when a user selects a value in a Combobox, the linked StringVar automatically updates. Setting a default for category_var ensures the app starts in the Temperature mode. This connects forward to UI creation because the UI widgets will be built using these variables (textvariable=...).


Step 6: Build the UI, Set Defaults, Focus Input, Bind Enter (Finish __init__)

Paste the following right after the variables (still inside __init__):

        # Build the UI
        self._build_ui()

        # Set defaults
        self._set_default_units()
        self._update_unit_dropdowns()

        # Focus input on launch
        self.value_entry.focus_set()

        # Enter triggers Convert
        self.bind("<Return>", lambda event: self.convert())
Code language: PHP (php)

This completes the setup flow inside __init__. First, _build_ui() creates all widgets (labels, dropdowns, buttons, output area). After widgets exist, _set_default_units() chooses friendly defaults (like Celsius → Fahrenheit), and _update_unit_dropdowns() fills the dropdown lists based on the selected category. focus_set() places the cursor in the input field so the user can type immediately. Finally, binding Enter to convert() makes the app feel like a real tool. The next step defines _build_ui(), which must live inside the same class.


Step 7: Create the _build_ui() Method (Inside the Same Class)

Now you’ll define the UI builder method. Paste this after __init__, still inside the class:

    # ---------------------------- UI BUILDING ----------------------------
    def _build_ui(self):
        padding = 12

        container = ttk.Frame(self, padding=padding)
        container.pack(fill="both", expand=True)

        # Title
        title = ttk.Label(container, text="Unit Converter", font=("Segoe UI", 16, "bold"))
        title.grid(row=0, column=0, columnspan=4, sticky="w", pady=(0, 10))

        # Category
        ttk.Label(container, text="Category:").grid(row=1, column=0, sticky="w")
        self.category_combo = ttk.Combobox(
            container,
            textvariable=self.category_var,
            values=list(self.units_by_category.keys()),
            state="readonly",
            width=18,
        )
        self.category_combo.grid(row=1, column=1, sticky="w", padx=(0, 10))
        self.category_combo.bind("<<ComboboxSelected>>", self.on_category_change)

        # Value input
        ttk.Label(container, text="Value:").grid(row=2, column=0, sticky="w", pady=(10, 0))
        self.value_entry = ttk.Entry(container, textvariable=self.value_var, width=22)
        self.value_entry.grid(row=2, column=1, sticky="w", pady=(10, 0), padx=(0, 10))
        ttk.Label(container, text="Example: 10.5").grid(row=2, column=2, columnspan=2, sticky="w", pady=(10, 0))

        # From / To
        ttk.Label(container, text="From Unit:").grid(row=3, column=0, sticky="w", pady=(10, 0))
        self.from_combo = ttk.Combobox(container, textvariable=self.from_unit_var, state="readonly", width=22)
        self.from_combo.grid(row=3, column=1, sticky="w", pady=(10, 0), padx=(0, 10))

        ttk.Label(container, text="To Unit:").grid(row=4, column=0, sticky="w", pady=(10, 0))
        self.to_combo = ttk.Combobox(container, textvariable=self.to_unit_var, state="readonly", width=22)
        self.to_combo.grid(row=4, column=1, sticky="w", pady=(10, 0), padx=(0, 10))

        # Buttons
        btn_frame = ttk.Frame(container)
        btn_frame.grid(row=5, column=0, columnspan=4, sticky="w", pady=(14, 0))

        ttk.Button(btn_frame, text="Convert", command=self.convert).grid(row=0, column=0, padx=(0, 8))
        ttk.Button(btn_frame, text="Swap", command=self.swap_units).grid(row=0, column=1, padx=(0, 8))
        ttk.Button(btn_frame, text="Clear", command=self.clear).grid(row=0, column=2, padx=(0, 8))
        ttk.Button(btn_frame, text="Exit", command=self.destroy).grid(row=0, column=3)

        # Output area
        self.output_label = ttk.Label(
            container,
            text="Enter a value and choose units, then click Convert.",
            font=("Segoe UI", 10),
            justify="left",
        )
        self.output_label.grid(row=6, column=0, columnspan=4, sticky="w", pady=(16, 0))

        # Improve grid stretching
        container.columnconfigure(3, weight=1)
Code language: PHP (php)

This entire method belongs to the class because it is indented under class UnitConverterApp(...). It connects backward because __init__ calls self._build_ui(). Inside _build_ui, you create a container frame and then place widgets using grid() so everything lines up like a form. Every widget uses the StringVar() variables from earlier (category_var, value_var, etc.), which is why those variables had to be created in __init__ first. This snippet connects forward because some UI elements call methods you still need to define next, such as on_category_change, convert, swap_units, and clear.


Step 8: Add the Category Change Handler (Inside the Same Class)

Paste this after _build_ui():

    # ---------------------------- EVENT HANDLERS ----------------------------
    def on_category_change(self, event=None):
        """When category changes, refresh unit dropdowns and set reasonable defaults."""
        self._set_default_units()
        self._update_unit_dropdowns()
        self.clear_output_only()
        self.value_entry.focus_set()
Code language: PHP (php)

This method is referenced in _build_ui() via self.category_combo.bind(...), so it must exist in the same class. When the category changes, you reset the default units (so the user immediately gets a sensible From/To pair), refresh the dropdown contents, clear old output text, and return focus to the input. The next snippet will define the core actions like convert() that the Convert button and Enter key depend on.


Step 9: Add convert() (Inside the Same Class)

Paste this next:

    # ---------------------------- CORE ACTIONS ----------------------------
    def convert(self):
        """Convert the given value from one unit to another."""
        raw_value = self.value_var.get().strip()
        if not raw_value:
            messagebox.showerror("Input Error", "Please enter a value to convert.\nExample: 10.5")
            self.value_entry.focus_set()
            return

        try:
            value = float(raw_value)
        except ValueError:
            messagebox.showerror("Input Error", "Please enter a valid number.\nExample: 10.5")
            self.value_entry.focus_set()
            return

        category = self.category_var.get()
        from_unit = self.from_unit_var.get()
        to_unit = self.to_unit_var.get()

        if not from_unit or not to_unit:
            messagebox.showerror("Selection Error", "Please select both From Unit and To Unit.")
            return

        # If same units, no conversion needed
        if from_unit == to_unit:
            self._show_result(value, from_unit, value, to_unit, note="No conversion needed.")
            return

        try:
            if category == "Temperature":
                self._validate_temperature(value, from_unit)
                result = self.convert_temperature(value, from_unit, to_unit)
            elif category == "Length":
                result = self.convert_length(value, from_unit, to_unit)
            elif category == "Weight":
                result = self.convert_weight(value, from_unit, to_unit)
            else:
                messagebox.showerror("Error", "Unknown category selected.")
                return

            self._show_result(value, from_unit, result, to_unit)

        except ValueError as exc:
            messagebox.showerror("Conversion Error", str(exc))
            return
Code language: PHP (php)

This method is called in three places you already created: the Convert button (command=self.convert), the Enter key binding (self.bind(...)), and the logic flow that shows results. It reads the value from the Entry (value_var), validates it, and reads the user’s selections from the dropdown variables. It then routes conversion based on category. This connects forward because it calls methods you haven’t added yet: _show_result, _validate_temperature, convert_temperature, convert_length, and convert_weight. Next, you’ll add the Swap/Clear actions that the UI buttons use.


Step 10: Add swap_units() and clear() (Inside the Same Class)

Paste this next:

    def swap_units(self):
        """Swap From Unit and To Unit."""
        from_u = self.from_unit_var.get()
        to_u = self.to_unit_var.get()
        if from_u and to_u:
            self.from_unit_var.set(to_u)
            self.to_unit_var.set(from_u)
            self.clear_output_only()
            self.value_entry.focus_set()

    def clear(self):
        """Clear input and output, but keep the chosen category."""
        self.value_var.set("")
        self.clear_output_only()
        self.value_entry.focus_set()
Code language: PHP (php)

These methods are directly connected to the Swap and Clear buttons you already created in _build_ui(). swap_units() simply flips the From and To selections so users can reverse a conversion quickly. clear() resets only the input value (not the category), which keeps the user’s context intact. Both methods call clear_output_only() next, so the next snippet defines that helper.


Step 11: Add clear_output_only() (Inside the Same Class)

Paste this next:

    def clear_output_only(self):
        self.output_label.config(text="Enter a value and choose units, then click Convert.")
Code language: PHP (php)

This small helper is used in multiple places (category change, swap, clear) to reset the output area consistently. It depends on self.output_label which was created in _build_ui(), so _build_ui() must be defined earlier. Next, you’ll add dropdown helper methods that are called during app startup and category changes.


Step 12: Add Dropdown Helpers _update_unit_dropdowns() and _set_default_units()

Paste this next:

    # ---------------------------- DROPDOWN HELPERS ----------------------------
    def _update_unit_dropdowns(self):
        """Update the From/To unit dropdowns based on the selected category."""
        category = self.category_var.get()
        units = self.units_by_category.get(category, [])

        self.from_combo["values"] = units
        self.to_combo["values"] = units

        # If current selection isn't valid for this category, reset it
        if self.from_unit_var.get() not in units:
            self.from_unit_var.set(units[0] if units else "")
        if self.to_unit_var.get() not in units:
            self.to_unit_var.set(units[1] if len(units) > 1 else (units[0] if units else ""))

    def _set_default_units(self):
        """Set friendly default conversions per category."""
        category = self.category_var.get()

        if category == "Temperature":
            self.from_unit_var.set("Celsius (°C)")
            self.to_unit_var.set("Fahrenheit (°F)")
        elif category == "Length":
            self.from_unit_var.set("Meters (m)")
            self.to_unit_var.set("Kilometers (km)")
        elif category == "Weight":
            self.from_unit_var.set("Grams (g)")
            self.to_unit_var.set("Kilograms (kg)")
Code language: PHP (php)

These methods connect backward to __init__ (startup) and on_category_change() (category switching). _set_default_units() chooses a beginner-friendly first conversion, while _update_unit_dropdowns() actually fills the dropdown lists. This ensures the UI always shows valid units. Next, you’ll add output formatting helpers because convert() depends on _show_result().


Step 13: Add Result Formatting Methods _show_result, _format_value, _unit_suffix

Paste this next:

    # ---------------------------- OUTPUT FORMATTING ----------------------------
    def _show_result(self, original_value, from_unit, converted_value, to_unit, note=None):
        """Display result nicely in the output label."""
        orig_str = self._format_value(original_value)
        conv_str = self._format_value(converted_value)

        lines = [
            f"Original:  {orig_str} {self._unit_suffix(from_unit)}",
            f"Converted: {conv_str} {self._unit_suffix(to_unit)}",
        ]
        if note:
            lines.append(f"Note: {note}")

        self.output_label.config(text="\n".join(lines))

    @staticmethod
    def _format_value(val):
        """Format numeric value to 2 decimals, but keep integers readable."""
        # Always show 2 decimals for a consistent professional look
        return f"{val:.2f}"

    @staticmethod
    def _unit_suffix(unit_text):
        """Extract the unit symbol/short form from dropdown label."""
        # Example: "Celsius (°C)" -> "°C"
        if "(" in unit_text and ")" in unit_text:
            return unit_text.split("(")[-1].replace(")", "").strip()
        return unit_text
Code language: PHP (php)

convert() calls _show_result() to present output consistently. _format_value() standardizes numbers to two decimals so the UI looks neat across all conversions. _unit_suffix() extracts the short unit symbol from labels like Celsius (°C) so output stays compact. This connects forward because temperature conversions require validation, so next you’ll add _validate_temperature().


Step 14: Add Temperature Validation _validate_temperature()

Paste this next:

    # ---------------------------- VALIDATION ----------------------------
    def _validate_temperature(self, value, from_unit):
        """Reject temperature values below absolute zero."""
        if from_unit == "Celsius (°C)" and value < self.ABS_ZERO_C:
            raise ValueError(f"Temperature cannot be below absolute zero ({self.ABS_ZERO_C} °C).")
        if from_unit == "Fahrenheit (°F)" and value < self.ABS_ZERO_F:
            raise ValueError(f"Temperature cannot be below absolute zero ({self.ABS_ZERO_F} °F).")
        if from_unit == "Kelvin (K)" and value < self.ABS_ZERO_K:
            raise ValueError("Temperature cannot be below absolute zero (0 K).")
Code language: PHP (php)

This method is called inside convert() only when the category is Temperature. It uses the constants you defined at the top of the class, which is why those constants were created before any methods. If validation fails, it raises ValueError, and convert() catches it and shows it using messagebox.showerror. Next, you’ll add the actual conversion logic methods.


Step 15: Add Temperature Conversion Methods

Paste this next:

    # ---------------------------- CONVERSION LOGIC ----------------------------
    # Temperature
    def convert_temperature(self, value, from_unit, to_unit):
        """Convert between Celsius, Fahrenheit, Kelvin."""
        # Step 1: Convert input to Celsius
        celsius = self._to_celsius(value, from_unit)

        # Step 2: Convert Celsius to target
        return self._from_celsius(celsius, to_unit)

    def _to_celsius(self, value, from_unit):
        if from_unit == "Celsius (°C)":
            return value
        if from_unit == "Fahrenheit (°F)":
            return (value - 32) * 5 / 9
        if from_unit == "Kelvin (K)":
            return value - 273.15
        raise ValueError("Unsupported temperature unit.")

    def _from_celsius(self, celsius, to_unit):
        if to_unit == "Celsius (°C)":
            return celsius
        if to_unit == "Fahrenheit (°F)":
            return (celsius * 9 / 5) + 32
        if to_unit == "Kelvin (K)":
            return celsius + 273.15
        raise ValueError("Unsupported temperature unit.")
Code language: PHP (php)

convert_temperature() is called from convert() when the category is Temperature. The two-step strategy (convert to Celsius first, then to the target) avoids writing separate formulas for every pair. _to_celsius() and _from_celsius() are helper methods that keep the main method readable. Next, you’ll add length and weight conversions, which use a “base unit factor” approach.


Step 16: Add Length Conversion convert_length()

Paste this next:

    # Length
    def convert_length(self, value, from_unit, to_unit):
        """Convert between supported length units using meters as a base."""
        to_meters_factor = {
            "Meters (m)": 1.0,
            "Kilometers (km)": 1000.0,
            "Centimeters (cm)": 0.01,
            "Millimeters (mm)": 0.001,
            "Inches (in)": 0.0254,
            "Feet (ft)": 0.3048,
        }

        if from_unit not in to_meters_factor or to_unit not in to_meters_factor:
            raise ValueError("Unsupported length unit selection.")

        meters = value * to_meters_factor[from_unit]
        result = meters / to_meters_factor[to_unit]
        return result
Code language: PHP (php)

This method is called from convert() when the user selects the Length category. It converts the input into a base unit (meters) and then converts from meters to the target unit. The dictionary stores conversion factors in one place so the math stays consistent and easy to expand later. Next, you’ll add weight conversion with the same approach.


Step 17: Add Weight Conversion convert_weight()

Paste this next:

    # Weight
    def convert_weight(self, value, from_unit, to_unit):
        """Convert between supported weight units using grams as a base."""
        to_grams_factor = {
            "Grams (g)": 1.0,
            "Kilograms (kg)": 1000.0,
            "Pounds (lb)": 453.59237,
            "Ounces (oz)": 28.349523125,
        }

        if from_unit not in to_grams_factor or to_unit not in to_grams_factor:
            raise ValueError("Unsupported weight unit selection.")

        grams = value * to_grams_factor[from_unit]
        result = grams / to_grams_factor[to_unit]
        return result
Code language: PHP (php)

This method completes the conversion set that convert() routes to. Like length, it uses a base unit (grams) so the process is consistent: convert input to grams, then convert grams to the target unit. At this point, the entire class is complete. Next, you’ll add the application entry point so the program can actually start.


Step 18: Add main() and the __name__ Guard (Outside the Class)

Now move to the very bottom of the file and paste this without class indentation:

def main():
    app = UnitConverterApp()
    app.mainloop()


if __name__ == "__main__":
    main()
Code language: JavaScript (javascript)

This snippet is not indented under the class, so it sits at the file level. main() creates the app instance, which triggers __init__, which builds the UI and sets defaults. mainloop() starts Tkinter’s event loop, which keeps the window open and responsive to button clicks and dropdown changes. The if __name__ == "__main__": guard ensures the app runs only when the file is executed directly.


By the time you finish copying all snippets:

  • Your file starts with imports.
  • Then a class UnitConverterApp(tk.Tk) begins.
  • Inside that class you have:
    • constants
    • __init__
    • _build_ui
    • on_category_change
    • convert, swap_units, clear, clear_output_only
    • _update_unit_dropdowns, _set_default_units
    • _show_result, _format_value, _unit_suffix
    • _validate_temperature
    • convert_temperature, _to_celsius, _from_celsius
    • convert_length, convert_weight
  • Finally, outside the class, you have main() and the __name__ guard.

If your file matches that order, you’ve built the full project directly from the article snippets.

Output