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
Last updated: 24 September 2026
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:
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:
$ 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:
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
@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
| Annotation | Means |
|---|---|
name: str | A 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() -> None | A 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
Orderdoes.TypedDictdescribes dictionaries with fixed keys, such as JSON you pass around without converting to objects. - Protocols. A
Protocoldescribes "anything with aread()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:
- Run the checker on the whole codebase with default settings and fix what it reports.
- Annotate new code and any function you touch, starting with public functions and data structures.
- Tighten settings module by module, for example requiring annotations on all functions in a package that is fully typed.
- 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?
mypy or Pyright?
Should every variable be annotated?
Can type hints check data from an API or a file?
What does Any mean?
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
- typing — Support for type hints — Python documentation
- PEP 484 – Type Hints — Python Enhancement Proposals
- PEP 604 – Allow writing union types as X | Y — Python Enhancement Proposals
- mypy: Getting started — mypy documentation
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.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.