Python Type Hints: Writing Cleaner, Safer Code

What Python type hints do and do not do, shown on a small module with three hidden bugs that mypy finds before the code runs: annotating functions and variables, None and union types, generic collections, dataclasses, narrowing, and how to add hints gradually to an existing codebase.

GloryTecks Team

Training & Placement Faculty

6 May 2026 6 min read

Last updated: 24 September 2026

Share:

Python checks types when your code runs, not before. A function that expects a number and receives a string fails only when that line executes, which may be in production, on the one path your tests did not cover. Type hints let you write down what each function expects and returns, and a type checker reads those notes and reports mismatches across the whole codebase without running anything. The interpreter itself ignores them: annotations are for tools and readers (Python docs: typing).

Three bugs, found before running

Here is a small module that looks fine at a glance:

python
from dataclasses import dataclass
from typing import Optional


@dataclass
class Order:
    order_id: int
    city: str
    amount: float
    coupon: Optional[str] = None


def total(orders: list[Order]) -> float:
    return sum(o.amount for o in orders)


def discount_code(order: Order) -> str:
    return order.coupon.upper()


def find(orders: list[Order], order_id: int) -> Order | None:
    for o in orders:
        if o.order_id == order_id:
            return o
    return None


orders = [Order(1, "Pune", 450.0), Order(2, "Hyderabad", "1200")]
print(total(orders))
match = find(orders, 3)
print(match.city)

Running mypy, the most widely used checker (mypy: getting started), on this file:

text
$ mypy orders.py
orders.py:18: error: Item "None" of "str | None" has no attribute "upper"  [union-attr]
orders.py:28: error: Argument 3 to "Order" has incompatible type "str"; expected "float"  [arg-type]
orders.py:31: error: Item "None" of "Order | None" has no attribute "city"  [union-attr]
Found 3 errors in 1 file (checked 1 source file)

Running the file with Python instead stops at the first problem it actually reaches:

text
TypeError: unsupported operand type(s) for +: 'float' and 'str'

That runtime error is the "1200" string being added to a float inside total, far from the line that caused it. The other two bugs, a coupon that may be None and a lookup that may find nothing, are still waiting. The checker found all three, pointed at the lines responsible, and never ran the code.

The fixes the checker asks for

python
@dataclass
class Order:
    order_id: int
    city: str
    amount: float
    coupon: str | None = None


def discount_code(order: Order) -> str:
    return order.coupon.upper() if order.coupon else ""

orders = [Order(1, "Pune", 450.0), Order(2, "Hyderabad", 1200.0)]
match = find(orders, 3)
print(match.city if match is not None else "no such order")

With the same total and find, mypy now reports Success: no issues found in 1 source file, and the program prints 1650.0 and no such order. Each fix is a decision the original code had silently skipped: what should happen when there is no coupon, and what should happen when no order matches.

The annotations you will use most

AnnotationMeans
name: strA string
items: list[int]A list of integers (built-in generics, Python 3.9+)
prices: dict[str, float]A dictionary from strings to floats
point: tuple[float, float]A pair of floats
code: Optional[str]A string or None; written with the union operator from Python 3.10
value: Union[int, str]Either an int or a string; also written with the union operator
def f() -> NoneA function that returns nothing
callback: Callable[[int], str]A function taking an int and returning a string

The X | Y syntax for unions arrived in Python 3.10 (PEP 604); on older versions, Optional[str] and Union[int, str] from typing mean the same thing. Annotate function parameters and return values first. Local variables rarely need annotations, because the checker infers their types from what is assigned.

Narrowing: how the checker follows your logic

A value typed Order | None cannot be used as an Order until you prove it is not None. Checks such as if match is not None:, if order.coupon: or isinstance(value, str) narrow the type inside that branch, which is why the fixed code passes. This is the most valuable thing a checker does in practice: it makes every "this might be missing" case visible and forces a decision about it.

Beyond the basics

  • Dataclasses and TypedDict. Dataclasses give records real field types, as Order does. TypedDict describes dictionaries with fixed keys, such as JSON you pass around without converting to objects.
  • Protocols. A Protocol describes "anything with a read() method" without requiring a common base class: structural typing that matches Python's duck typing.
  • Generics. Functions like "return the first item of any list" can be typed so the result has the same type as the list's items.
  • Runtime validation is separate. Hints never check data arriving from outside. For JSON from an API or rows from a file, validate at the boundary with a library such as Pydantic, which reads the same annotations at run time; see Pydantic explained.

Adding hints to existing code

Typing is gradual by design: annotations are optional, and unannotated code keeps working (PEP 484). mypy does not even check the bodies of functions without annotations by default. A workable path for an existing project:

  1. Run the checker on the whole codebase with default settings and fix what it reports.
  2. Annotate new code and any function you touch, starting with public functions and data structures.
  3. Tighten settings module by module, for example requiring annotations on all functions in a package that is fully typed.
  4. Run the checker in continuous integration, so the gains are not lost.

Editors use the same annotations for autocompletion and inline errors, so the benefits start on the first day, before the codebase is fully typed.

Do type hints make Python faster?

No. CPython ignores annotations at run time, apart from storing them. Their value is in finding bugs and documenting intent.

mypy or Pyright?

Both are mature static type checkers. mypy is the long-standing reference implementation; Pyright is fast and powers type checking in many editors. Pick one per project and run it in CI.

Should every variable be annotated?

No. Annotate function signatures and class attributes; let the checker infer local variables. Add a local annotation only when inference cannot work it out, such as an empty list that will be filled later.

Can type hints check data from an API or a file?

Not on their own. A checker only sees your code, and at run time Python does not enforce annotations, so a JSON payload with a string where you declared a float passes straight through. Validate external data when it arrives, then rely on the types inside your program.

What does Any mean?

A type that the checker does not check at all. It is an escape hatch for code that cannot be typed yet; each Any is a place where bugs can hide, so keep them few and deliberate.

Type hints document functions whose arguments can be subtle, as in Python *args and **kwargs, and pair naturally with the dataclasses in the object-oriented programming guide. A checker is one of the tools that keep a codebase consistent, alongside the formatter and linter described in Python best practices. Clean, well-tested Python is the focus of the testing and best practices module in the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course