Day 21 · ~17m

Python Type Hints: Write Code That Documents Itself

Add type hints to functions and variables. Learn why they don't enforce at runtime yet why every professional codebase uses them to catch bugs at edit time.

student (curious)

Okay so I was reading Amir's code this morning. His function signatures look like this: def process_orders(orders: list[dict], discount: float) -> list[dict]: What's all that extra stuff? The colons and the arrow?

teacher (neutral)

Type hints. They tell you what types the function expects and what it returns.

student (thinking)

Does Python enforce them? Like, if I pass a string instead of a float, does it crash?

teacher (serious)

No. Python is still dynamically typed. You can pass a string, an integer, a list. Python doesn't care. The interpreter ignores type hints entirely.

student (confused)

Then why would Amir write them? If Python doesn't enforce them, what's the point?

teacher (focused)

Because they are instructions for three things Python doesn't do: your IDE, static analysis tools, and your future self reading the code six months from now.

Watch this. Without type hints:

def calculate_order_summary(orders):
    # What is orders? A list? A tuple? Dict keys?
    # What does this return?
    return {"count": len(orders), "total": sum(o["price"] for o in orders)}

With type hints:

def calculate_order_summary(orders: list[dict]) -> dict:
    # Instantly clear: list of dicts in, dict out
    return {"count": len(orders), "total": sum(o["price"] for o in orders)}
student (thinking)

The second one is easier to read. I know what it expects and what it returns without reading the body.

teacher (encouraging)

Now your IDE can help you. When you call the function, your editor autocompletes the parameter name. When you try to assign the return value to a string, your editor warns you before you run the code.

student (curious)

Wait, how does the IDE know the type if Python doesn't enforce it?

teacher (neutral)

Your IDE is not Python. It is a different tool — like mypy, a static type checker — that reads your code and follows the hints to find mistakes before you run the program.

result = calculate_order_summary(orders)  # returns dict
price = result.upper()  # IDE says: dict has no method upper()
student (thinking)

So the IDE uses the type hints to catch bugs I would have found by running the code and seeing it crash.

teacher (proud)

Exactly. You find the bug at edit time, not at runtime.

student (excited)

What do the basic type hints look like?

teacher (focused)

Start with the simple ones:

def greet(name: str) -> str:
    return f"Hello, {name}"

def add(a: int, b: int) -> int:
    return a + b

def calculate_discount(price: float, percent: float) -> float:
    return price * (1 - percent / 100)

def is_active(user: bool) -> bool:
    return user
student (thinking)

So name: str means "name is a string" and -> str means "returns a string."

teacher (neutral)

Yes. For collections:

def get_order_ids(orders: list) -> list:
    return [o["id"] for o in orders]

But this is vague. A list of what?

def get_order_ids(orders: list[dict]) -> list[str]:
    return [o["id"] for o in orders]

Now it is clear: list of dicts in, list of strings out.

student (focused)

What about dictionaries?

teacher (focused)

Same pattern:

def get_user_map(users: list[dict]) -> dict[str, str]:
    # dict[key_type, value_type]
    return {u["email"]: u["name"] for u in users}
student (curious)

So dict[str, str] means "dict with string keys and string values." What if the values are different types?

teacher (serious)

That is where things get more interesting. You have a few options. One: use a Union:

from typing import Union

def get_config() -> dict[str, Union[str, int, bool]]:
    # Values can be string, int, or bool
    return {"name": "app", "port": 8000, "debug": True}

Or use Any if it really can be anything:

from typing import Any

def parse_json(data: str) -> dict[str, Any]:
    # Could be anything
    return {"value": 42, "name": "test", "nested": [1, 2, 3]}
student (thinking)

What about None? I remember None being a type of problem in the code.

teacher (encouraging)

Great question. If a function might return None, use Optional:

from typing import Optional

def find_order(order_id: int) -> Optional[dict]:
    # Returns dict or None
    if order_id < 0:
        return None
    return {"id": order_id, "total": 99.99}
student (curious)

Optional means it could be the type or None?

teacher (focused)

Yes. Optional[dict] is shorthand for Union[dict, None]. But Optional is clearer.

student (thinking)

Can I add type hints to variables too, not just functions?

teacher (neutral)

You can:

order_id: int = 42
customer_name: str = "Priya"
prices: list[float] = [9.99, 19.99, 29.99]
configuration: dict[str, int] = {"retries": 3, "timeout": 30}
student (amused)

But Python can figure out that 42 is an int without me telling it.

teacher (amused)

It can. Variable type hints are useful when the type is not obvious, or when you want to be explicit about what you intend:

# Unclear
total = some_calculation()

# Clear
total: float = some_calculation()
student (thinking)

So I am writing type hints for tools and for future me, not for Python.

teacher (encouraging)

Exactly. Python ignores them at runtime. Your IDE, your static checker, and anyone reading your code uses them.

student (curious)

What happens if I ignore type hints and just write code?

teacher (serious)

The code works fine. Python runs it. But without hints, your IDE cannot autocomplete, static checkers cannot find bugs, and six months later when you read your own function signature, you have to guess what it expects.

student (thinking)

And Amir probably has type hints in his code because he knows how much they help.

teacher (proud)

He has them because he spent years writing code without them and then switched. He will never go back.

student (focused)

All right. Let me add type hints to that Order class from last week. Every method needs a hint about what it takes and returns.

teacher (encouraging)

That is exactly where they shine. A class method with hints is self-documenting:

class Order:
    def __init__(self, order_id: int, total: float) -> None:
        self.order_id = order_id
        self.total = total
    
    def apply_discount(self, percent: float) -> float:
        return self.total * (1 - percent / 100)
    
    def is_valid(self) -> bool:
        return self.total > 0
student (excited)

So -> None means the method does not return anything.

teacher (proud)

Exactly right. Now your IDE knows this method is for side effects, not for its return value.

student (thinking)

Can I use type hints with default arguments?

teacher (focused)

Yes:

def send_email(recipient: str, subject: str, body: str = "Default message") -> bool:
    # The = sets the default, the : sets the type
    return True
student (curious)

One more thing. I see Amir use something like list[str] but I thought he had to import stuff from typing.

teacher (neutral)

Depends on the Python version. Before 3.9, you needed from typing import List and wrote List[str]. Now you can write list[str] directly. Amir probably uses a recent version.

student (proud)

I can add those to my code right now.

teacher (excited)

And your IDE will immediately start helping you catch mistakes before you run the code. That is the entire point of type hints.

student (thinking)

Is there a tool that enforces them? Like mypy?

teacher (serious)

mypy is a static type checker. You run it and it finds type errors without executing code:

mypy my_code.py

It reads your hints and reports mismatches. Many professional codebases run mypy in CI — if your types do not check, your PR cannot merge.

student (amused)

So I write hints so my IDE helps, and the team uses mypy to catch my mistakes before code review.

teacher (amused)

And you spend less time debugging because the tools caught the error at edit time instead of at 2 AM when the database query fails.

student (focused)

All right, let me implement the challenge. I need to add type hints to a function.

teacher (encouraging)

Start with the function signature. What does it take, what does it return. Once you know that, the hints are straightforward.