Learning Objectives
By the end of this lesson, you will understand how Python type hints make programs easier to read, maintain, test, and develop.
You will learn how to:
- Understand the purpose of type hints.
- Annotate variables.
- Add types to function parameters.
- Specify function return types.
- Type lists, dictionaries, tuples, and sets.
- Work with optional and multiple types.
-
Use
Anyappropriately. - Annotate custom classes.
- Create reusable type aliases.
-
Understand generic types and
Callable. - Understand the role of static type checkers.
What Are Type Hints?
Type hints are optional annotations that describe the expected type of a variable, parameter, or return value.
For example:
name: str = "Ali"
age: int = 25
price: float = 1499.99
active: bool = True
These annotations tell readers and development tools what kind of value is expected.
Python generally does not enforce ordinary type hints at runtime. They are primarily used to make code clearer and to help static analysis tools, IDEs, and developers.
Why Type Hints Matter
Python is dynamically typed, which means you normally do not have to declare a variable's type.
name = "Ali"
age = 25
This is convenient, but larger applications can become harder to understand.
Compare:
def calculate_total(price, quantity):
return price * quantity
with:
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
The second version communicates the intended interface much more clearly.
Benefits
- Better readability
- Better IDE autocomplete
- Earlier detection of type-related mistakes
- Easier maintenance
- Better documentation
- Improved collaboration
Variable Annotations
A variable annotation uses a colon followed by the expected type.
username: str = "Sara"
age: int = 28
salary: float = 85000.50
is_active: bool = True
The annotation itself does not automatically convert or validate the value.
age: int = "twenty-five"
print(age)
Python will generally allow this assignment at runtime, even though it conflicts with the annotation.
A type hint describes an intended type; it is not the same thing as runtime type enforcement.
Basic Type Hints
Python's common built-in types can be used directly as annotations.
name: str = "Ali"
age: int = 30
height: float = 5.9
verified: bool = False
nothing: None = None
Common built-in types
-
str -
int -
float -
bool -
bytes -
None
Type Hints for Function Parameters
Function parameters can be annotated to describe what type of value the function expects.
def greet(name: str):
print(f"Hello, {name}!")
greet("Ali")
Multiple parameters can each have their own annotation.
def calculate_area(
width: float,
height: float
):
return width * height
Type hints effectively document the expected interface of the function.
Return Type Hints
The arrow syntax -> describes the expected
return type.
def add(a: int, b: int) -> int:
return a + b
Another example:
def calculate_price(
price: float,
quantity: int
) -> float:
return price * quantity
The annotation after -> tells readers and
static analysis tools what the function is expected to
return.
Combining Parameter and Return Types
Professional functions often annotate both their parameters and their return values.
def calculate_discount(
price: float,
percentage: float
) -> float:
return price * (percentage / 100)
This clearly communicates:
-
priceshould be a float. -
percentageshould be a float. - The function is expected to return a float.
None as a Return Type
If a function is designed to return no meaningful
value, annotate it with None.
def display_message(message: str) -> None:
print(message)
This communicates that the function performs an action rather than producing a useful return value.
def save_user(name: str) -> None:
print(f"Saving {name}")
Type Hints for Collections
Modern Python allows you to describe not only the collection itself, but also the types of values stored inside it.
numbers: list[int] = [10, 20, 30]
names: list[str] = [
"Ali",
"Sara",
"Ahmed"
]
This is more informative than simply writing:
numbers: list = [10, 20, 30]
The first version tells us what the elements inside the list are expected to be.
Typing Lists
Use list[type] to describe a list of a
particular type.
scores: list[int] = [
75,
88,
92,
81
]
A function can also accept a typed list.
def calculate_average(
scores: list[float]
) -> float:
return sum(scores) / len(scores)
This tells us that the function expects a list whose elements are numbers represented as floats.
Typing Dictionaries
Dictionaries can be annotated with both key and value types.
prices: dict[str, float] = {
"Laptop": 1200.50,
"Phone": 800.00,
"Mouse": 25.99
}
Here:
- Keys are strings.
- Values are floats.
Another example:
def get_prices() -> dict[str, float]:
return {
"Laptop": 1200.50,
"Phone": 800.00
}
Typing Tuples
Tuples can contain fixed types in fixed positions.
person: tuple[str, int] = (
"Ali",
25
)
This means the first element is expected to be a string and the second an integer.
A tuple containing an arbitrary number of values of the same type can use:
numbers: tuple[int, ...] = (
10,
20,
30,
40
)
Typing Sets
Sets can also be annotated using the generic
set syntax.
unique_ids: set[int] = {
101,
102,
103
}
For strings:
categories: set[str] = {
"Electronics",
"Clothing",
"Books"
}
Union Types
Sometimes a value can legitimately have more than one possible type.
Modern Python allows the pipe operator:
def format_id(
value: int | str
) -> str:
return str(value)
This means value may be either an
int or a str.
The same concept can be used with variables.
user_id: int | str = 101
user_id = "USR-101"
The X | Y syntax is the modern,
concise way to express a union of types.
Optional Values
A value that can either contain a specific type or
None can be represented with a union.
def find_user(
user_id: int
) -> str | None:
if user_id == 1:
return "Ali"
return None
The return annotation tells us that the function may return a string or no value.
Older Python code often used:
from typing import Optional
def find_user(
user_id: int
) -> Optional[str]:
...
The modern str | None form is generally
preferred in current Python versions.
The Any Type
Any indicates that a value may be of any
type.
from typing import Any
data: Any = 100
data = "Python"
data = [1, 2, 3]
This can be useful when working with genuinely dynamic data.
Using Any everywhere removes much of
the benefit of static type checking. Prefer a more
precise type whenever possible.
Type Hints with Custom Classes
Your own classes can be used as types.
class Product:
def __init__(
self,
name: str,
price: float
):
self.name = name
self.price = price
def display_product(
product: Product
) -> str:
return f"{product.name}: ${product.price:.2f}"
Now the function communicates that it expects a
Product object.
laptop = Product(
"Laptop",
1200.00
)
print(display_product(laptop))
Laptop: $1200.00
Type Aliases
A type alias gives a meaningful name to a more complex type.
type ProductPrice = dict[str, float]
You can then use the alias:
prices: ProductPrice = {
"Laptop": 1200.0,
"Phone": 800.0
}
Type aliases are particularly useful when the same complex type appears throughout a project.
A good type alias can make complicated code much easier to understand.
Generic Types
Generic types allow reusable code to work with different types while preserving type information.
def first_item[T](items: list[T]) -> T:
return items[0]
Conceptually, T represents whatever type
the list contains.
numbers = [10, 20, 30]
result = first_item(numbers)
print(result)
10
Generics become increasingly useful when building reusable libraries, frameworks, and strongly typed application code.
The Callable Type
Callable is useful when a function accepts
another function as an argument.
from collections.abc import Callable
def execute(
operation: Callable[[int, int], int],
a: int,
b: int
) -> int:
return operation(a, b)
def add(a: int, b: int) -> int:
return a + b
result = execute(add, 10, 20)
print(result)
30
Here, Callable[[int, int], int] describes
a function that accepts two integers and returns an
integer.
Static Type Checking
Type hints become especially powerful when combined with static type-checking tools.
Popular tools include:
- mypy
- Pyright
- IDE type analysis
Consider:
def add(a: int, b: int) -> int:
return a + b
result = add(
"10",
"20"
)
Python may still execute this call because annotations are not ordinary runtime enforcement.
However, a static type checker can identify that strings are being passed where integers were intended.
Type hints + IDE support + static analysis can catch many mistakes before your program is deployed.
Common Type-Hint Mistakes
Mistake 1 — Thinking type hints enforce types
Type hints normally do not automatically prevent incorrect runtime values.
Mistake 2 — Using Any everywhere
Excessive use of Any removes useful type
information.
Mistake 3 — Writing inaccurate annotations
def calculate_total(
price: str,
quantity: int
) -> str:
return price * quantity
Annotations should describe what the function actually expects and returns.
Mistake 4 — Overcomplicating simple types
Type hints should improve readability rather than make simple code unnecessarily complicated.
Mistake 5 — Ignoring collection element types
products: list = []
When possible, make the contents explicit:
products: list[str] = []
Practice Exercises
Exercise 1 — Basic Function
Add type hints to a function that receives a name and age and returns a formatted string.
Exercise 2 — Shopping Cart
Create a function that accepts
list[float] and returns the total as a
float.
Exercise 3 — Dictionary
Create a typed dictionary containing product names and prices.
Exercise 4 — Optional Result
Create a function that searches for a user and
returns str | None.
Exercise 5 — Custom Class
Create a Student class and use it as
the parameter type of another function.
Exercise 6 — Callable
Write a function that accepts another function and applies it to two integers.
Mini Challenge — Typed Ecommerce Product System
Build a small typed product system for an ecommerce application.
class Product:
def __init__(
self,
name: str,
price: float,
stock: int
):
self.name = name
self.price = price
self.stock = stock
def calculate_inventory_value(
products: list[Product]
) -> float:
total = 0
for product in products:
total += (
product.price *
product.stock
)
return total
products: list[Product] = [
Product(
"Laptop",
1200.00,
5
),
Product(
"Phone",
800.00,
10
),
Product(
"Keyboard",
75.00,
20
)
]
total = calculate_inventory_value(products)
print(f"Inventory value: ${total:,.2f}")
Expected Output
Inventory value: $15,000.00
Extend the Challenge
- Create a type alias for the product list.
-
Create a function that returns
Product | None. - Create a typed dictionary for product statistics.
-
Create a
Callable-based discount function. - Add type hints to every function and variable you create.
- Run a static type checker against your program.
Knowledge Check
Test your understanding of Python type hints.
1. What is the main purpose of type hints?
2. Which annotation represents a string?
3. What does -> float indicate in a function?
4. How do you annotate a list of integers?
5. What does int | str mean?
6. What does str | None commonly represent?
7. What is Any?
8. Which tool can perform static type checking?
9. Are normal Python type hints automatically enforced at runtime?
10. What does Callable help describe?
Lesson Summary
Type hints provide a structured way to communicate the expected types used throughout a Python application.
Key Concepts
- Type hints describe intended types.
-
Variable annotations use
name: type. - Function parameters can be annotated.
-
Return values use the
->syntax. - Collections can describe the types of their elements.
-
list[int]describes a list of integers. -
dict[str, float]describes string keys and float values. -
int | strrepresents multiple possible types. -
str | Nonerepresents an optional string result. -
Anyshould be used carefully. - Custom classes can be used as types.
- Type aliases make complex types easier to read.
- Generics allow reusable type-safe structures.
-
Callablecan describe functions passed as arguments. - Static type checkers can identify many type-related problems before runtime.
Type hints become especially valuable as applications grow. In the next lesson, we will use them together with another powerful Python feature: Dataclasses.
Finished this lesson?
Mark this lesson complete when you are comfortable using type hints in variables, functions, and collections.