Type hints¶
In this tutorial, you will learn how to add type annotations to your function parameters and return values. Type hints make your code more readable, help your editor provide better autocompletion, and enable static analysis tools to catch bugs before your code runs.
Time commitment: 15–20 minutes
Prerequisites:
- Python 3.12 or later installed on your machine
- Completion of Tutorial 01 — Defining functions
- Completion of Tutorial 02 — Lambda expressions
Learning objectives¶
By the end of this tutorial, you will be able to:
- Add type annotations to function parameters and return values
- Use common types from the
typingmodule - Understand that type hints are not enforced at runtime
- Use
OptionalandUniontypes for flexible function signatures
What are type hints?¶
Type hints (also called type annotations) are a way to indicate the expected types of function parameters and return values. They were introduced in Python 3.5 through PEP 484.
Type hints do not change how your code runs — Python does not enforce them at runtime. Instead, they serve as documentation for humans and tools:
- They make your code easier to read and understand
- They help editors and IDEs provide better autocompletion and error checking
- They allow static analysis tools like
mypyto find type-related bugs
Think of type hints as labels that tell anyone reading your code what kind of data a function expects and what it gives back.
Your first type hint¶
Here is a simple function without type hints:
def greet(name):
return f"Hello, {name}!"
print(greet("Alice"))
Now let us add type hints. You annotate parameters with a colon and the type, and the return value with an arrow (->) before the colon:
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("Alice"))
The function behaves exactly the same way. The type hints name: str and -> str simply communicate that name should be a string and that the function returns a string.
Common built-in types¶
Python has several built-in types that you can use directly in type hints. Here are the most common ones:
def add(a: int, b: int) -> int:
return a + b
def divide(a: float, b: float) -> float:
return a / b
def is_positive(number: int) -> bool:
return number > 0
print(add(3, 5))
print(divide(10.0, 3.0))
print(is_positive(-7))
The common built-in types you will use most often are:
| Type | Description | Example values |
|---|---|---|
int |
Whole numbers | 42, -7, 0 |
float |
Decimal numbers | 3.14, -0.5 |
str |
Text strings | "hello", "Python" |
bool |
Boolean values | True, False |
list |
Ordered collections | [1, 2, 3] |
dict |
Key-value mappings | {"name": "Alice"} |
tuple |
Immutable sequences | (1, "hello") |
Type hints for parameters with defaults¶
When a parameter has a default value, the type hint goes before the = sign:
def greet(name: str = "World") -> str:
return f"Hello, {name}!"
print(greet())
print(greet("Bob"))
The type hint name: str = "World" tells you three things at once: the parameter is called name, it should be a str, and it defaults to "World" if no argument is provided.
The list, dict, and tuple types¶
Since Python 3.9, you can use the built-in list, dict, and tuple types directly in type hints with square brackets to specify what they contain. This is called parameterised or generic typing.
def calculate_average(numbers: list[float]) -> float:
return sum(numbers) / len(numbers)
print(calculate_average([85.0, 92.5, 78.0, 95.5]))
def count_words(text: str) -> dict[str, int]:
word_counts = {}
for word in text.lower().split():
word_counts[word] = word_counts.get(word, 0) + 1
return word_counts
print(count_words("the cat sat on the mat"))
def get_name_and_age() -> tuple[str, int]:
return ("Alice", 30)
name, age = get_name_and_age()
print(f"{name} is {age} years old.")
Here is a summary of the parameterised types:
| Type hint | Meaning |
|---|---|
list[int] |
A list of integers |
list[str] |
A list of strings |
dict[str, int] |
A dictionary with string keys and integer values |
tuple[str, int] |
A tuple containing exactly one string and one integer |
tuple[int, ...] |
A tuple of any number of integers |
Optional parameters¶
Sometimes a parameter can either have a value or be None. In Python 3.10 and later, you can express this with the X | None syntax:
def find_user(user_id: int) -> str | None:
users = {1: "Alice", 2: "Bob", 3: "Charlie"}
return users.get(user_id)
print(find_user(1))
print(find_user(99))
The return type str | None means the function returns either a string or None. This is very common for functions that search for something and may not find it.
You can also use Optional from the typing module, which does exactly the same thing. This is the older syntax that works in all Python 3.x versions:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob", 3: "Charlie"}
return users.get(user_id)
print(find_user(2))
print(find_user(50))
Optional[str] is equivalent to str | None. Both forms are correct. If you are using Python 3.10 or later, the | syntax is generally preferred as it is more concise.
Union types¶
Sometimes a parameter can accept more than one type. In Python 3.10 and later, use the | operator:
def double(value: int | float) -> int | float:
return value * 2
print(double(5))
print(double(3.14))
The type hint int | float means the parameter accepts either an integer or a floating-point number.
For older Python versions, you can use Union from the typing module:
from typing import Union
def double(value: Union[int, float]) -> Union[int, float]:
return value * 2
print(double(10))
print(double(2.5))
Union[int, float] and int | float are equivalent. As with Optional, the | syntax is the modern approach.
Type hints are not enforced¶
It is important to understand that Python does not enforce type hints at runtime. They are purely informational. If you pass the wrong type, Python will not raise a TypeError because of the annotation — it will only raise an error if the operation itself fails.
def add(a: int, b: int) -> int:
return a + b
# Passing strings instead of integers — no type error from the hints
result = add("hello", " world")
print(result)
print(type(result))
The function accepted strings even though the type hints say int. Python executed the + operator on the strings (which concatenates them) without complaint.
This is why type hints are called "hints" — they are suggestions, not rules. To actually enforce types, you would use a static analysis tool like mypy, which checks your code without running it.
Benefits of type hints¶
Even though Python does not enforce type hints, they provide significant benefits:
Better IDE support. Editors like VS Code use type hints to provide autocompletion, parameter information, and inline error detection.
Self-documenting code. Type hints tell readers what a function expects without having to read the implementation.
Static analysis. Tools like
mypycan analyse your code and catch type-related bugs before you run the program.Easier refactoring. When you change a function's signature, type hints help you find all the places that need updating.
Let us see how type hints serve as documentation by comparing two versions of the same function:
# Without type hints — what does this function expect?
def create_profile(name, age, email, active):
return {"name": name, "age": age, "email": email, "active": active}
# With type hints — much clearer
def create_profile(name: str, age: int, email: str, active: bool = True) -> dict[str, str | int | bool]:
return {"name": name, "age": age, "email": email, "active": active}
profile = create_profile("Alice", 30, "alice@example.com")
print(profile)
With the type hints in place, you can immediately see that name is a string, age is an integer, email is a string, and active is a boolean that defaults to True.
Exercise¶
The following functions have no type hints. Add appropriate type annotations to each parameter and return value.
def calculate_total(prices, tax_rate):
subtotal = sum(prices)
return round(subtotal * (1 + tax_rate), 2)
def find_longest(words):
if not words:
return None
longest = words[0]
for word in words[1:]:
if len(word) > len(longest):
longest = word
return longest
def build_greeting(name, formal):
if formal:
return f"Good day, {name}."
return f"Hi, {name}!"
# Add type hints to each function below
def calculate_total(prices, tax_rate):
subtotal = sum(prices)
return round(subtotal * (1 + tax_rate), 2)
def find_longest(words):
if not words:
return None
longest = words[0]
for word in words[1:]:
if len(word) > len(longest):
longest = word
return longest
def build_greeting(name, formal):
if formal:
return f"Good day, {name}."
return f"Hi, {name}!"
# Test your annotated functions
# print(calculate_total([19.99, 9.99, 4.99], 0.2))
# print(find_longest(["cat", "elephant", "dog"]))
# print(find_longest([]))
# print(build_greeting("Alice", True))
# print(build_greeting("Bob", False))
Solution¶
def calculate_total(prices: list[float], tax_rate: float) -> float:
subtotal = sum(prices)
return round(subtotal * (1 + tax_rate), 2)
def find_longest(words: list[str]) -> str | None:
if not words:
return None
longest = words[0]
for word in words[1:]:
if len(word) > len(longest):
longest = word
return longest
def build_greeting(name: str, formal: bool) -> str:
if formal:
return f"Good day, {name}."
return f"Hi, {name}!"
print(calculate_total([19.99, 9.99, 4.99], 0.2))
print(find_longest(["cat", "elephant", "dog"]))
print(find_longest([]))
print(build_greeting("Alice", True))
print(build_greeting("Bob", False))
Key points from the solution:
calculate_totaltakes alist[float]and afloat, and returns afloatfind_longestreturnsstr | Nonebecause it returnsNonewhen the list is emptybuild_greetingtakes astrand abool, and always returns astr
Summary¶
In this tutorial, you learned how to:
- Add type annotations to function parameters using
parameter: typesyntax - Specify return types using the
-> typesyntax - Use common built-in types:
int,float,str,bool,list,dict, andtuple - Annotate parameters with default values
- Use parameterised types like
list[int]anddict[str, int] - Express optional values with
str | NoneorOptional[str] - Express union types with
int | floatorUnion[int, float] - Understand that type hints are not enforced at runtime
What is next¶
In the next tutorial, Docstrings, you will learn how to document your functions using docstrings. Combined with type hints, docstrings make your code clear and easy to use for anyone — including your future self.