Type Hints¶
Type hints define a function's expected interface, forming a contract between the function and its callers. They make the expected types of inputs and outputs explicit.
Mental Model
Type hints are labels on a function's inputs and outputs. Python does not enforce them at runtime---they are documentation that tools like mypy can check statically. Think of them as a contract: "I expect a float in and promise a float out." They make code self-documenting and catch type mismatches before the program runs.
The Problem¶
Consider this function:
python
def area(length, width):
return length * width
What types are length and width?
Are they integers, floats, or something else?
Without additional context, the reader cannot tell.
The Solution¶
Type hints make the expected types explicit.
python
def area(length: float, width: float) -> float:
return length * width
A reader can immediately see the intended types.
Syntax¶
Type hints appear after parameter names and after the function arrow.
python
def add(a: int, b: int) -> int:
return a + b
Here:
a: intmeansashould be an integerb: intmeansbshould be an integer-> intmeans the function returns an integer
A slightly richer example:
```python def average(a: float, b: float) -> float: return (a + b) / 2
result = average(3.0, 7.0) print(result) ```
Output
text
5.0
Returning None¶
Functions that do not return a value can be annotated with -> None.
python
def greet(name: str) -> None:
print("Welcome,", name)
This tells the reader — and any analysis tool — that greet is called for its side effect (printing), not for a return value.
Type Hints Are Optional¶
Python does not enforce type hints at runtime.
This means the following will still run without error:
```python def add(a: int, b: int) -> int: return a + b
print(add("2", "3")) ```
Output
text
23
The function was annotated for int, but Python happily accepted two strings and concatenated them.
The result "23" is valid Python but almost certainly not what the caller intended.
Static analysis tools such as mypy or pyright can examine the code without running it and warn about mismatches like this before they cause bugs.
Key Ideas¶
Type hints document the expected types of parameters (: type) and return values (-> type).
Python does not enforce them at runtime, but they make code easier to read and allow static analysis tools to catch type errors early.
For functions that only perform side effects and return nothing, use -> None.
Notebook Examples¶
```python def celsius_to_fahrenheit(c: float) -> float: """Convert a temperature from Celsius to Fahrenheit.""" return (c * 9 / 5) + 32
print(help(celsius_to_fahrenheit)) ```
```python def convert_celsius_to_fahrenheit(c: float) -> float: """Convert a temperature from Celsius to Fahrenheit.""" return (c * 9 / 5) + 32
print(help(convert_celsius_to_fahrenheit)) ```
Exercises¶
Exercise 1. Type hints are NOT enforced at runtime. Predict the output:
```python def add(a: int, b: int) -> int: return a + b
print(add(3, 4)) print(add("hello", " world")) print(add([1], [2])) ```
All three calls succeed. Why does Python not enforce type hints? What tool would catch the last two calls as errors? What is the benefit of type hints if they are not enforced?
Solution to Exercise 1
Output:
text
7
hello world
[1, 2]
All three succeed because Python ignores type hints at runtime. The hints are metadata stored on the function object (add.__annotations__), but Python's interpreter does not check them when the function is called.
A static type checker like mypy or pyright would flag the last two calls as errors: add("hello", " world") passes str where int is expected.
The benefits of type hints without enforcement:
- Documentation: readers immediately see expected types.
- IDE support: autocompletion, refactoring, and inline error highlighting.
- Static analysis: tools catch type errors before runtime, during development.
- Gradual adoption: existing code works without changes; hints can be added incrementally.
Exercise 2. A programmer annotates a function incorrectly:
```python def divide(a: int, b: int) -> int: return a / b
result = divide(7, 2) print(result, type(result)) ```
What does the function actually return? Does the type hint -> int change the return type? What should the correct annotation be?
Solution to Exercise 2
Output:
text
3.5 <class 'float'>
The / operator always returns float in Python 3, regardless of the operand types. The type hint -> int does NOT change the actual return type -- it is just a claim by the programmer, and in this case, the claim is wrong.
The correct annotation should be -> float:
python
def divide(a: int, b: int) -> float:
return a / b
Or, if integer division is intended:
python
def divide(a: int, b: int) -> int:
return a // b
This illustrates an important point: type hints are only as correct as the programmer makes them. A wrong hint is worse than no hint because it misleads readers and tools.
Exercise 3.
Type hints can express complex types using typing module constructs. Explain what each annotation means:
```python from typing import Optional, Union
def find(items: list[int], target: int) -> Optional[int]: ...
def process(value: Union[str, int]) -> str: ...
def transform(data: list[tuple[str, int]]) -> dict[str, int]: ... ```
What is the difference between Optional[int] and Union[int, None]? Why is expressing these types in hints useful even though Python does not check them?
Solution to Exercise 3
-
find(items: list[int], target: int) -> Optional[int]: takes a list of integers and an integer target. Returns either anint(the found value/index) orNone(if not found). -
process(value: Union[str, int]) -> str: accepts either a string or an integer. Returns a string. -
transform(data: list[tuple[str, int]]) -> dict[str, int]: takes a list of (string, integer) tuples. Returns a dictionary mapping strings to integers.
Optional[int] is exactly equivalent to Union[int, None] -- it means "int or None." Optional is just syntactic sugar for the common pattern of "this value might be None." In Python 3.10+, you can write int | None instead.
Expressing these types is useful because it communicates the contract of the function: what it accepts and what it returns, including edge cases like None. This helps other developers use the function correctly and helps static analysis tools verify that callers handle all possible return types (e.g., checking for None before using the result).
Next: Docstrings.