PyLearn Learn Python Professionally
Home / Lessons / Python Type Hints
Lesson 54 · Advanced Python Concepts

Python Type Hints

Learn how to describe the expected types of variables, function parameters, return values, collections, and complex data structures using Python's type-hinting system.

45–55 minutes Intermediate Advanced Python Concepts
0%
01

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 Any appropriately.
  • Annotate custom classes.
  • Create reusable type aliases.
  • Understand generic types and Callable.
  • Understand the role of static type checkers.
02

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.

Important:

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.

03

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
04

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.

Remember:

A type hint describes an intended type; it is not the same thing as runtime type enforcement.

05

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
06

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.

07

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.

08

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:

  • price should be a float.
  • percentage should be a float.
  • The function is expected to return a float.
09

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.

Example:
def save_user(name: str) -> None:
    print(f"Saving {name}")
10

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.

11

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.

12

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
    }
13

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
)
14

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"
}
15

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"
Modern Python:

The X | Y syntax is the modern, concise way to express a union of types.

16

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.

17

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.

Use Any carefully.

Using Any everywhere removes much of the benefit of static type checking. Prefer a more precise type whenever possible.

18

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
19

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.

Design principle:

A good type alias can make complicated code much easier to understand.

20

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.

21

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.

22

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.

Professional workflow:

Type hints + IDE support + static analysis can catch many mistakes before your program is deployed.

23

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] = []
24

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.

25

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

  1. Create a type alias for the product list.
  2. Create a function that returns Product | None.
  3. Create a typed dictionary for product statistics.
  4. Create a Callable-based discount function.
  5. Add type hints to every function and variable you create.
  6. Run a static type checker against your program.
26

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?

27

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 | str represents multiple possible types.
  • str | None represents an optional string result.
  • Any should be used carefully.
  • Custom classes can be used as types.
  • Type aliases make complex types easier to read.
  • Generics allow reusable type-safe structures.
  • Callable can 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.