Python Type Checking and Type Hinting

Run a static type checker, narrow optional values, describe collections and structural interfaces, and read type errors.

Lesson 63 of 107Advanced12 min read

Static type checking means a tool reads your type hints and reports mismatches before the program runs, such as passing a string where a number is expected, or using a value that might be None. Python itself ignores hints at run time, so you need a checker: mypy, Pyright (used by VS Code's Pylance) or a similar tool.

Python
def shipping_weight(weights: list[float]) -> float:
    return sum(weights)

shipping_weight("2.5 kg")   # runs and fails at run time; mypy reports it before
Text
error: Argument 1 to "shipping_weight" has incompatible type "str"; expected "list[float]"

The type hints and dataclasses lesson introduced annotation syntax. This lesson is about using a checker in practice: running it, handling optional values, describing collections, and expressing interfaces with Protocol. The examples come from a parcel depot weighing packages.

Static intent

Annotations state what a function expects and returns:

Python
def parcel_cost(weight_kg: float, express: bool = False) -> float:
    rate = 4.5 if express else 2.0
    return round(weight_kg * rate, 2)

Install and run a checker on your project:

Terminal
python -m pip install mypy
mypy depot/

mypy reports each problem with a file, line and explanation, and exits with a non-zero status, which makes it easy to run in continuous integration. Functions without annotations are skipped by default, so you can add hints gradually, module by module; --strict turns on the full set of checks once a codebase is ready.

Annotations remain hints: parcel_cost("3") still runs until the multiplication fails, because Python doesn't check them when the program runs. If you need run-time validation of external data, use explicit checks or a library such as Pydantic.

Optional values

A value that may be absent is written X | None (or Optional[X] before Python 3.10). The checker then insists that you handle the None case before using the value:

Python
def find_weight(parcels: dict[str, float], code: str) -> float | None:
    return parcels.get(code)

weight = find_weight({"P1": 2.5}, "P9")
print(weight * 2)          # mypy: Unsupported operand types for * ("None" and "int")

Fix it by narrowing: a check that rules out None, after which the checker knows the type is float:

Python
if weight is None:
    print("Unknown parcel")
else:
    print(weight * 2)      # weight is float here

isinstance() checks, early returns and assert weight is not None narrow types in the same way. This catches the most common Python crash, 'NoneType' object has no attribute... or unsupported operand errors, at the desk instead of in production.

Collection types

Collection annotations describe the contents as well as the container:

AnnotationMeaning
list[float]a list of floats
dict[str, float]string keys, float values
tuple[str, float]exactly a string then a float
tuple[float, ...]any number of floats
set[str]a set of strings
Iterable[float]anything you can loop over that yields floats

For function parameters, accepting the most general type you need makes functions more reusable: def total(weights: Iterable[float]) works with lists, tuples and generators. Return the specific type you build.

For dictionaries with fixed keys, such as a parsed JSON record, TypedDict describes each key's type:

Python
from typing import TypedDict

class Parcel(TypedDict):
    code: str
    weight: float

Annotations never convert data: ["2.5", "1.0"] annotated as list[float] is still a list of strings at run time.

Structural interfaces

A Protocol describes the methods an object must have, without requiring a common base class. It's static duck typing:

Python
from typing import Protocol

class Weighable(Protocol):
    def weight_kg(self) -> float: ...

class Box:
    def weight_kg(self) -> float:
        return 3.2

class Envelope:
    def weight_kg(self) -> float:
        return 0.1

def total_weight(items: list[Weighable]) -> float:
    return sum(item.weight_kg() for item in items)

Box and Envelope don't inherit from Weighable, yet the checker accepts them because their methods match. A class whose weight_kg returned a string, or that lacked the method, would be reported. Protocols are how type checkers describe built-in behaviours like "anything with __len__" (Sized), and they keep code flexible while still catching mistakes.

Independent practice and a worked check

Task: annotate package data, narrow an optional value and run a configured static checker on an incompatible call.

Python
# depot.py
def find_weight(parcels: dict[str, float], code: str) -> float | None:
    return parcels.get(code)

def postage(weight: float) -> float:
    return round(weight * 2.0, 2)

parcels = {"P1": 2.5, "P2": 0.4}
w = find_weight(parcels, "P2")
if w is not None:
    print(postage(w))       # 0.8
postage("heavy")            # mypy reports this line

Running mypy depot.py reports Argument 1 to "postage" has incompatible type "str"; expected "float" for the last line, while the narrowed call passes. For the worked check: do annotations alone enforce parameter types at run time? No. The program runs until the bad value actually causes an error.

Reference and next practice

The mypy documentation has a cheat sheet and guides for gradual adoption, and the typing module reference covers Protocol, TypedDict and more. Continue with dependency management and revisit PEP 8 style. Practise with the Python exercises and Python quiz.

Type Checking and Type Hinting Summary

  • Static intent: Type hints describe expected types for readers and static analysis tools.
  • Optional values: A union with None communicates that a value may be absent.
  • Collection types: Generic annotations describe the element or key and value types inside collections.
  • Structural interfaces: Protocols can describe supported operations without requiring one concrete inheritance chain.

Type Checking and Type Hinting Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Type Checking and Type Hinting in a small program

Python
def shipping_weight(weights: list[float]) -> float:
    return sum(weights)

print(shipping_weight([1.2, 0.8, 2.0]))
Output
4.0
Line-by-line explanation
  1. Line 1The parameter is a list of floats and the function returns a float.
  2. Line 4The call matches the annotation, so a checker reports no problems.
Example 2

Narrowing and protocols together

Python
from typing import Protocol

class Weighable(Protocol):
    def weight_kg(self) -> float: ...

class Box:
    def __init__(self, kg: float) -> None:
        self.kg = kg
    def weight_kg(self) -> float:
        return self.kg

class Envelope:
    def weight_kg(self) -> float:
        return 0.1

def heaviest(items: list[Weighable]) -> Weighable | None:
    if not items:
        return None
    return max(items, key=lambda item: item.weight_kg())

parcels: list[Weighable] = [Box(3.2), Envelope(), Box(7.5)]
top = heaviest(parcels)
if top is not None:
    print(f"Heaviest: {top.weight_kg()} kg")
print(heaviest([]))
Output
Heaviest: 7.5 kg
None
Line-by-line explanation
  1. Lines 3 to 4The protocol states the one method callers rely on.
  2. Lines 16 to 19The return type admits None, so callers must check before using the result.
  3. Lines 22 to 23After is not None, the checker knows top has weight_kg().

Common Type Checking and Type Hinting Mistakes

Using an optional value without checking it

Don't
def label(code: str) -> str:
    weight = find_weight(parcels, code)      # float | None
    return f"{code}: {weight * 1000:.0f} g"  # mypy error; crashes when None
Do
def label(code: str) -> str:
    weight = find_weight(parcels, code)
    if weight is None:
        return f"{code}: unknown"
    return f"{code}: {weight * 1000:.0f} g"

Silencing errors with Any

Don't
from typing import Any
def parcel_cost(weight: Any) -> Any:   # the checker can no longer help
    return weight * 2.0
Do
def parcel_cost(weight: float) -> float:
    return weight * 2.0

Any switches checking off for that value. Use it only at true boundaries, and narrow it as soon as possible.

Type Checking and Type Hinting in Real Projects

Type checking in real codebases

Large Python codebases at companies such as Dropbox, Instagram and many smaller teams run mypy or Pyright in continuous integration, often adopting it gradually: new code is annotated, and strictness increases module by module. Editors use the same hints for autocomplete, go-to-definition and inline error highlighting, which is a big part of why hints pay off even in small projects.

Libraries publish type information too, either inline or as separate "stub" packages such as types-requests, so the checker understands third-party APIs. Frameworks like FastAPI and Pydantic go further and use annotations at run time to validate requests and generate documentation.

Type Checking and Type Hinting FAQ

Does Python check types at run time?

No. Type hints are stored but not enforced when the program runs. A static checker such as mypy or Pyright reads them and reports problems beforehand. Libraries such as Pydantic and typeguard can enforce types at run time if you need it.

What is the difference between mypy and Pyright?

Both are static type checkers that understand the same annotations. mypy is the original Python project; Pyright, from Microsoft, is very fast and powers VS Code's Pylance extension. They occasionally differ on edge cases, so a project usually standardises on one.

What does "Item None of Optional has no attribute" mean?

The value's type includes None, and you used it without checking. Add a narrowing check such as if value is not None: or handle the None case first, and the error goes away.

Should I add type hints to every variable?

No. Annotate function parameters and return values, class attributes and anything the checker can't infer, such as an empty list that will hold specific items (results: list[float] = []). Most local variables are inferred automatically.

What is a Protocol in Python typing?

A Protocol describes the methods and attributes an object must provide. Any class with matching members satisfies it, without inheriting from it. It lets type checkers verify duck-typed code.

Type Checking and Type Hinting Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Do annotations alone enforce parameter types at runtime?

Multiple choicemedium

Which review concern is valid for type checking and type hinting?

Type Checking and Type Hinting Quiz

2 questions to check what you learned.

  1. Do annotations alone enforce parameter types at runtime?

    • Yes
    • No
    • Python ignores the distinction
  2. Which statement correctly describes collection types?

    • Generic annotations describe the element or key and value types inside collections.
    • Only the example output matters, not the rule
    • Every operation has the same behavior