EASYTUTORGUIDE

Practical tutorials, tools, courses, digital skills, and business promotion.

Free Learning

JavaScript – Chapter 2: Development Environment

Learn what JavaScript is, where it runs, why it is important, and how beginners can start using it with simple examples and clear output.

Beginner Friendly JavaScript Basics Web Development Code Examples
JavaScript Lesson 2 Chapter 2 Topics Day / Night Mode

Main reading content

python-course-chapter-33

Chapter 33: Type Hints and Static Typing

A complete beginner-friendly guide to Python type hints, annotations, static type checking, generic programming, protocols, typed dictionaries, modern type syntax, MyPy, Pyright, and practical typed application development.

Goal: Learn how to describe expected data types clearly, detect mistakes earlier, improve editor support, and make larger Python programs easier to understand and maintain.

Chapter 33 Topics

``` ```

33.1 Dynamic Typing Review

```

Python is a dynamically typed language. This means a variable does not permanently belong to one data type. A variable stores a reference to an object, and the same variable name can later refer to an object of another type.

Python determines types while the program runs. You do not need to declare every variable as an integer, string, or Boolean before using it. This makes Python flexible, but some mistakes may not be discovered until the affected code runs.

Example

# The variable first stores an integer
```

value = 25

print(value)
print(type(value))

# The same variable now stores a string

value = "Python"

print(value)
print(type(value))

# The same name can store a list later

value = [10, 20, 30]

print(value)
print(type(value))
```

Output

25
```


Python

[10, 20, 30]
```

Output Explanation

The name value first refers to an integer, then to a string, and finally to a list. Python allows this because types belong to objects rather than permanently belonging to variable names.

Possible Runtime Problem

price = "19.99"
```

quantity = 3

# This repeats the string instead of performing multiplication

print(price * quantity)
```

Output

19.9919.9919.99

The program runs, but the result is probably not what the programmer intended. Type hints can help editors and static type checkers identify this kind of mistake before execution.

```

33.2 Why Type Hints Are Used

```

Type hints describe which data types variables, function parameters, return values, and class attributes are expected to use. They improve readability because another programmer can understand how code should be called without studying every line of its implementation.

Type hints can also improve code completion, editor suggestions, documentation, refactoring, and error detection. They are especially valuable in larger projects where many functions, modules, and developers must work together.

Python normally does not enforce type hints automatically at runtime. A type hint is guidance for programmers and tools. Static type checkers can analyze the code and report mismatches before the program is executed.

Example Without Type Hints

def calculate_total(price, quantity):
return price * quantity
```

print(calculate_total(10.5, 3))
```

Example With Type Hints

def calculate_total(
price: float,
quantity: int
```

) -> float:
return price * quantity

total = calculate_total(10.5, 3)

print(total)
```

Output

31.5

Explanation

The annotations show that price should be a floating-point number, quantity should be an integer, and the function should return a floating-point number. This information makes the function easier to use correctly.

```

33.3 Variable Annotations

```

A variable annotation describes the expected type of a variable. The annotation is written after the variable name and a colon. The value is assigned after the equals sign in the usual way.

An annotation does not normally prevent a different type from being assigned while the program runs. However, a static type checker can report the assignment as a possible mistake.

Basic Syntax

variable_name: data_type = value

Example

student_name: str = "Sara"
```

student_age: int = 14
average_score: float = 88.5
is_active: bool = True

print(student_name)
print(student_age)
print(average_score)
print(is_active)
```

Output

Sara
```

14
88.5
True
```

Annotation Before Assignment

city: str
```

population: int

city = "Toronto"
population = 2794356

print(city)
print(population)
```

Output

Toronto
```

2794356
```

Incorrect Type Example

quantity: int = 5
```

# Python may run this assignment,

# but a type checker should report it.

quantity = "five"

print(quantity)
```

Runtime Output

five

This demonstrates that ordinary annotations do not automatically enforce types at runtime. Their main purpose is static analysis, documentation, and editor assistance.

```

33.4 Function Annotations

```

Function annotations describe the expected types of parameters and return values. Parameter annotations appear after parameter names. The return annotation appears after the closing parenthesis and an arrow.

These annotations communicate how the function is intended to be used. They also help static type checkers detect incorrect calls, such as passing a list where an integer is expected.

Example

def create_greeting(
name: str,
age: int
```

) -> str:
return f"Hello {name}. You are {age} years old."

message = create_greeting("Michael", 13)

print(message)
```

Output

Hello Michael. You are 13 years old.

Default Parameter Values

def calculate_price(
price: float,
quantity: int = 1,
tax_rate: float = 0.13
```

) -> float:
subtotal = price * quantity
tax = subtotal * tax_rate

```
return subtotal + tax
```

print(calculate_price(20))
print(calculate_price(20, 3))
print(calculate_price(20, 3, 0.05))
```

Output

22.6
```

67.8
63.0
```

Inspecting Annotations

print(calculate_price.__annotations__)

Output

{'price': <class 'float'>, 'quantity': <class 'int'>, 'tax_rate': <class 'float'>, 'return': <class 'float'>}
```

33.5 Return Types

```

A return type annotation describes the type of value a function is expected to return. It is written after the parameter list using an arrow. The annotation helps callers understand what they will receive.

Functions that do not return a useful value are usually annotated with None. Functions that may return more than one possible type can use a union type.

Returning a String

def format_name(
first_name: str,
last_name: str
```

) -> str:
return f"{first_name.title()} {last_name.title()}"

print(format_name("sara", "johnson"))
```

Output

Sara Johnson

Returning a Number

def average(
first: float,
second: float
```

) -> float:
return (first + second) / 2

print(average(80, 90))
```

Output

85.0

Returning Nothing

def display_message(message: str) -> None:
print(message)
```

result = display_message("Learning type hints")

print("Returned value:", result)
```

Output

Learning type hints
```

Returned value: None
```

The function displays a message but does not explicitly return another value. Python automatically returns None.

```

33.6 Collection Types

```

Collection annotations describe both the collection itself and the types of values it contains. Modern Python can annotate lists, tuples, sets, and dictionaries using the built-in collection names.

A list annotation such as list[str] means a list containing strings. A dictionary annotation such as dict[str, int] means string keys mapped to integer values.

List Example

student_names: list[str] = [
"Sara",
"Michael",
"Ali"
```

]

for name in student_names:
print(name)
```

Output

Sara
```

Michael
Ali
```

Dictionary Example

scores: dict[str, float] = {
"Sara": 92.5,
"Michael": 84.0,
"Ali": 88.5
```

}

for name, score in scores.items():
print(name, score)
```

Output

Sara 92.5
```

Michael 84.0
Ali 88.5
```

Tuple and Set Example

coordinate: tuple[float, float] = (
43.6532,
-79.3832
```

)

categories: set[str] = {
"Python",
"HTML",
"CSS"
}

print("Coordinate:", coordinate)
print("Categories:", categories)
```

Example Output

Coordinate: (43.6532, -79.3832)
```

Categories: {'Python', 'HTML', 'CSS'}
```

Variable-Length Tuple

scores: tuple[int, ...] = (
80,
90,
85,
95
```

)

print(scores)
```

Output

(80, 90, 85, 95)
```

33.7 Optional Types

```

An optional type means a value can contain a particular type or None. This is useful when a value may be missing, unknown, not found, or not yet assigned.

Modern Python commonly writes an optional string as str | None. Older code may use Optional[str] from the typing module. Both describe the same basic idea.

Modern Syntax

def find_student(
student_id: int
```

) -> str | None:
students = {
101: "Sara",
102: "Michael"
}

```
return students.get(student_id)
```

first_result = find_student(101)
second_result = find_student(999)

print(first_result)
print(second_result)
```

Output

Sara
```

None
```

Checking Before Use

student_name = find_student(101)
```

if student_name is not None:
print(student_name.upper())
else:
print("Student was not found.")
```

Output

SARA

Older Compatible Syntax

from typing import Optional
```

def get_middle_name(
student_id: int
) -> Optional[str]:
if student_id == 1:
return "James"

```
return None
```

33.8 Union Types

```

A union type means a value may be one of several allowed types. Modern Python uses the vertical bar to join the possible types. Older code often uses Union from the typing module.

Union types are useful when an input may arrive in more than one valid format. However, using too many unrelated types can make functions difficult to understand and should be avoided.

Example

def convert_to_float(
value: int | float | str
```

) -> float:
return float(value)

print(convert_to_float(10))
print(convert_to_float(4.5))
print(convert_to_float("19.99"))
```

Output

10.0
```

4.5
19.99
```

Processing Different Types

def describe_value(
value: int | str
```

) -> str:
if isinstance(value, int):
return f"Integer doubled: {value * 2}"

```
return f"Text uppercase: {value.upper()}"
```

print(describe_value(5))
print(describe_value("python"))
```

Output

Integer doubled: 10
```

Text uppercase: PYTHON
```

Older Syntax

from typing import Union
```

def display_id(
identifier: Union[int, str]
) -> None:
print(identifier)

33.9 Literal Types

```

A literal type restricts a value to a small set of exact values. It is useful when a parameter should accept only specific strings, numbers, or Boolean values.

Static type checkers can report a problem when a value outside the allowed set is passed. At runtime, ordinary Python does not automatically enforce the restriction, so validation may still be needed.

Example

from typing import Literal
```

OrderStatus = Literal[
"pending",
"processing",
"shipped",
"delivered"
]

def update_order(
order_id: int,
status: OrderStatus
) -> str:
return f"Order {order_id} changed to {status}."

print(update_order(1001, "shipped"))
```

Output

Order 1001 changed to shipped.

Literal Number Example

from typing import Literal
```

Rating = Literal[1, 2, 3, 4, 5]

def save_rating(rating: Rating) -> None:
print("Saved rating:", rating)

save_rating(5)
```

Output

Saved rating: 5

A type checker should report calls such as save_rating(10) because 10 is not one of the declared literal values.

```

33.10 Type Aliases

```

A type alias gives a descriptive name to a type expression. This makes long or repeated annotations easier to read and maintain.

Type aliases are helpful for coordinates, identifiers, callbacks, nested collections, database rows, and other structures used repeatedly throughout a program.

Basic Alias

StudentId = int
```

Score = float

student_id: StudentId = 101
student_score: Score = 92.5

print(student_id)
print(student_score)
```

Output

101
```

92.5
```

Complex Alias

StudentRecord = dict[str, str | int | float]
```

student: StudentRecord = {
"name": "Sara",
"age": 14,
"score": 92.5
}

print(student)
```

Output

{'name': 'Sara', 'age': 14, 'score': 92.5}

Explicit Alias Declaration

from typing import TypeAlias
```

Coordinate: TypeAlias = tuple[
float,
float
]

location: Coordinate = (
43.6532,
-79.3832
)

print(location)
```

Output

(43.6532, -79.3832)
```

33.11 Callable Types

```

A callable is an object that can be called like a function. The Callable type describes the parameter types and return type of a function passed into another function.

Callable annotations are useful for callbacks, event handlers, sorting functions, validators, formatters, and functions that receive other functions.

Example

from collections.abc import Callable
```

def add(
first: int,
second: int
) -> int:
return first + second

def multiply(
first: int,
second: int
) -> int:
return first * second

def calculate(
first: int,
second: int,
operation: Callable[[int, int], int]
) -> int:
return operation(first, second)

print(calculate(5, 3, add))
print(calculate(5, 3, multiply))
```

Output

8
```

15
```

No-Argument Callable

from collections.abc import Callable
```

def run_action(
action: Callable[[], None]
) -> None:
print("Starting action")
action()
print("Action finished")

def display_message() -> None:
print("Learning Python typing")

run_action(display_message)
```

Output

Starting action
```

Learning Python typing
Action finished

33.12 Generic Types

```

Generic types allow classes and functions to work with multiple data types while preserving information about the specific type being used.

A generic container may store integers in one part of a program and strings in another. Static type checkers can still understand the exact stored type for each instance.

Generic Class Example

from typing import Generic, TypeVar
```

T = TypeVar("T")

class Box(Generic[T]):
def **init**(self, value: T):
self.value = value

```
def get_value(self) -> T:
    return self.value
```

number_box = Box[int](100)
text_box = Box[str](%22Python%22)

print(number_box.get_value())
print(text_box.get_value())
```

Output

100
```

Python
```

Explanation

The number box stores an integer, so its get_value() method is understood to return an integer. The text box stores a string, so the same method is understood to return a string.

Generic Stack Example

from typing import Generic, TypeVar
```

ItemType = TypeVar("ItemType")

class Stack(Generic[ItemType]):
def **init**(self) -> None:
self._items: list[ItemType] = []

```
def push(self, item: ItemType) -> None:
    self._items.append(item)

def pop(self) -> ItemType:
    return self._items.pop()
```

stack = Stack[str]()

stack.push("First")
stack.push("Second")

print(stack.pop())
print(stack.pop())
```

Output

Second
```

First

33.13 Type Variables

```

A type variable represents a type that will be determined when a generic function or class is used. It helps connect input types to output types.

For example, a function that returns the first item from a list should return the same type stored in that list. A type variable expresses that relationship.

Example

from typing import TypeVar
```

T = TypeVar("T")

def get_first(items: list[T]) -> T:
return items[0]

first_number = get_first([10, 20, 30])
first_name = get_first(["Sara", "Michael"])

print(first_number)
print(first_name)
```

Output

10
```

Sara
```

Bound Type Variable

from typing import TypeVar
```

Number = TypeVar(
"Number",
int,
float
)

def add_values(
first: Number,
second: Number
) -> Number:
return first + second

print(add_values(10, 5))
print(add_values(2.5, 1.5))
```

Output

15
```

4.0
```

This type variable is restricted to integers and floating-point numbers. A static type checker can reject unrelated types.

```

33.14 Protocols

```

A protocol describes behavior an object must provide rather than requiring it to inherit from one particular base class. This supports structural typing, sometimes described as static duck typing.

An object satisfies a protocol when it provides the required attributes and methods. It does not need to explicitly inherit from the protocol.

Example

from typing import Protocol
```

class Printable(Protocol):
def print_details(self) -> None:
...

class Student:
def **init**(
self,
name: str,
score: float
) -> None:
self.name = name
self.score = score

```
def print_details(self) -> None:
    print(
        self.name,
        "-",
        self.score
    )
```

class Product:
def **init**(
self,
name: str,
price: float
) -> None:
self.name = name
self.price = price

```
def print_details(self) -> None:
    print(
        self.name,
        "- $",
        self.price
    )
```

def display_item(
item: Printable
) -> None:
item.print_details()

display_item(Student("Sara", 92))
display_item(Product("Keyboard", 49.99))
```

Output

Sara - 92
```

Keyboard - $ 49.99
```

Output Explanation

Both classes satisfy the protocol because both provide a compatible print_details() method. Neither class needs to inherit from Printable.

```

33.15 TypedDict

```

A TypedDict describes the expected keys and value types of a dictionary. At runtime, the value remains an ordinary dictionary, but static type checkers understand its required structure.

Typed dictionaries are useful for JSON-like data, configuration objects, API responses, database rows, and structured records that should remain dictionaries.

Example

from typing import TypedDict
```

class StudentRecord(TypedDict):
student_id: int
name: str
score: float
active: bool

student: StudentRecord = {
"student_id": 101,
"name": "Sara",
"score": 92.5,
"active": True
}

print(student["name"])
print(student["score"])
```

Output

Sara
```

92.5
```

Optional Keys

from typing import NotRequired, TypedDict
```

class ProductRecord(TypedDict):
name: str
price: float
description: NotRequired[str]

first_product: ProductRecord = {
"name": "Keyboard",
"price": 49.99
}

second_product: ProductRecord = {
"name": "Mouse",
"price": 24.99,
"description": "Wireless mouse"
}

print(first_product)
print(second_product)
```

Output

{'name': 'Keyboard', 'price': 49.99}
```

{'name': 'Mouse', 'price': 24.99, 'description': 'Wireless mouse'}

33.16 NewType

```

NewType creates a distinct type for static checking while keeping the same underlying runtime value. It is useful for values that share the same basic Python type but have different meanings.

For example, a student identifier and an order identifier may both be integers. Creating separate types helps prevent accidentally using one in place of the other.

Example

from typing import NewType
```

StudentId = NewType(
"StudentId",
int
)

OrderId = NewType(
"OrderId",
int
)

def find_student(
student_id: StudentId
) -> str:
return f"Looking for student {student_id}"

student_id = StudentId(101)
order_id = OrderId(5001)

print(find_student(student_id))
print(type(student_id))
print(type(order_id))
```

Output

Looking for student 101
```


```

Output Explanation

Both values remain integers at runtime. However, a static type checker can distinguish StudentId from OrderId and report incorrect usage.

```

33.17 Final

```

Final tells static type checkers that a variable or attribute should not be reassigned. It is commonly used for constants and configuration values.

The annotation does not usually prevent reassignment at runtime. It communicates the programmer's intention and allows type checkers to report later assignments as mistakes.

Example

from typing import Final
```

TAX_RATE: Final[float] = 0.13
APPLICATION_NAME: Final[str] = "Student Manager"
MAX_ATTEMPTS: Final[int] = 3

print(APPLICATION_NAME)
print(TAX_RATE)
print(MAX_ATTEMPTS)
```

Output

Student Manager
```

0.13
3
```

Final Class Attribute

from typing import Final
```

class Settings:
VERSION: Final[str] = "1.0.0"

print(Settings.VERSION)
```

Output

1.0.0

A type checker should report code that tries to assign a new value to one of these final names.

```

33.18 ClassVar

```

ClassVar identifies an attribute that belongs to the class rather than to each individual object. It helps distinguish shared class data from instance data.

This is useful in data classes and ordinary classes where some values, such as counters or category names, should be shared by every instance.

Example

from typing import ClassVar
```

class Student:
school_name: ClassVar[str] = (
"Python Learning School"
)

```
student_count: ClassVar[int] = 0

def __init__(
    self,
    name: str
) -> None:
    self.name = name
    Student.student_count += 1
```

first_student = Student("Sara")
second_student = Student("Michael")

print(first_student.name)
print(second_student.name)
print(Student.school_name)
print(Student.student_count)
```

Output

Sara
```

Michael
Python Learning School
2
```

Data Class Example

from dataclasses import dataclass
```

from typing import ClassVar

@dataclass
class Product:
category: ClassVar[str] = "General"
name: str
price: float

product = Product(
"Keyboard",
49.99
)

print(product)
print(Product.category)
```

Output

Product(name='Keyboard', price=49.99)
```

General

33.19 Self Types

```

The Self type represents the current class. It is useful for methods that return the current object or another object of the same class.

It is especially helpful in fluent interfaces, builder methods, class methods, and inheritance. Subclasses can preserve their more specific type instead of always being treated as the base class.

Example

from typing import Self
```

class Student:
def **init**(
self,
name: str,
score: float = 0
) -> None:
self.name = name
self.score = score

```
def set_score(
    self,
    score: float
) -> Self:
    self.score = score
    return self

def display(self) -> Self:
    print(
        self.name,
        self.score
    )

    return self
```

student = (
Student("Sara")
.set_score(92)
.display()
)
```

Output

Sara 92

Class Method Example

from typing import Self
```

class Product:
def **init**(
self,
name: str,
price: float
) -> None:
self.name = name
self.price = price

```
@classmethod
def free_product(
    cls,
    name: str
) -> Self:
    return cls(name, 0.0)
```

sample = Product.free_product(
"Sample"
)

print(sample.name)
print(sample.price)
```

Output

Sample
```

0.0

33.20 Modern Python Type Syntax

```

Modern Python provides shorter type syntax than older versions. Built-in collection names can be used directly, union types can use the vertical bar, and newer Python versions support a dedicated type statement for aliases.

The syntax used in a project should match the oldest Python version the project supports. Code intended for older Python versions may need imports from typing.

Older and Modern Collection Syntax

# Older style
```

from typing import Dict, List

old_names: List[str] = [
"Sara",
"Michael"
]

old_scores: Dict[str, int] = {
"Sara": 92,
"Michael": 81
}

# Modern style

new_names: list[str] = [
"Sara",
"Michael"
]

new_scores: dict[str, int] = {
"Sara": 92,
"Michael": 81
}

print(new_names)
print(new_scores)
```

Output

['Sara', 'Michael']
```

{'Sara': 92, 'Michael': 81}
```

Older and Modern Union Syntax

from typing import Optional, Union
```

# Older syntax

old_value: Union[int, str] = 10
old_optional: Optional[str] = None

# Modern syntax

new_value: int | str = "Python"
new_optional: str | None = None

print(old_value)
print(old_optional)
print(new_value)
print(new_optional)
```

Output

10
```

None
Python
None
```

Modern Type Alias Statement

# Supported in newer Python versions
```

type Coordinate = tuple[float, float]
type StudentScores = dict[str, float]

location: Coordinate = (
43.6532,
-79.3832
)

scores: StudentScores = {
"Sara": 92.5
}

print(location)
print(scores)
```

Output

(43.6532, -79.3832)
```

{'Sara': 92.5}

33.21 Static Type Checkers

```

A static type checker examines source code without needing to execute every program path. It compares annotations with assignments, function calls, return values, attribute access, and collection contents.

Static type checking can identify many mistakes earlier, but it cannot prove that every program is correct. Runtime validation, testing, exception handling, and careful program design are still necessary.

Example with Type Mistakes

def calculate_total(
price: float,
quantity: int
```

) -> float:
return price * quantity

price: float = "19.99"
quantity: int = 3

total = calculate_total(
price,
quantity
)

print(total)
```

Possible Type Checker Report

error: Incompatible types in assignment
   expression has type "str"
   variable has type "float"

Corrected Version

def calculate_total(
price: float,
quantity: int
```

) -> float:
return price * quantity

price: float = 19.99
quantity: int = 3

total: float = calculate_total(
price,
quantity
)

print(total)
```

Output

59.97

What Static Checkers Can Find

  • Incorrect argument types
  • Incorrect return types
  • Missing dictionary keys in typed dictionaries
  • Incorrect collection item types
  • Access to attributes that do not exist
  • Possible use of None without checking
  • Incorrect method overrides
  • Reassignment of final values
```

33.22 MyPy

```

MyPy is a popular static type checker for Python. It reads annotations and reports situations where values do not match the declared types.

MyPy is normally installed separately. It can check one file, a folder, or a complete project. It does not replace Python and usually does not run the application itself.

Install MyPy

python -m pip install mypy

Example File

# filename: type_example.py
```

def greet(name: str) -> str:
return "Hello " + name

student_name: str = "Sara"

print(greet(student_name))
```

Run MyPy

python -m mypy type_example.py

Successful Output

Success: no issues found in 1 source file

Example with an Error

def greet(name: str) -> str:
return "Hello " + name
```

print(greet(100))
```

Possible MyPy Output

type_example.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
```

Found 1 error in 1 file
```

Strict Checking

python -m mypy --strict type_example.py

Strict mode enables additional checks. It is useful for carefully typed projects, but beginners may prefer to add strict rules gradually.

```

33.23 Pyright

```

Pyright is another static type checker for Python. It is known for fast analysis and strong editor integration. It is commonly used through command-line tools or editor extensions.

Pyright and MyPy follow similar typing concepts, but they may produce different messages or interpret some advanced cases differently. Projects often select one main checker and configure it consistently.

Example Program

# filename: pyright_example.py
```

def calculate_discount(
price: float,
rate: float
) -> float:
return price * rate

price: float = 100.0
discount: float = calculate_discount(
price,
0.20
)

print(discount)
```

Output

20.0

Example Type Error

def calculate_discount(
price: float,
rate: float
```

) -> float:
return price * rate

discount = calculate_discount(
"one hundred",
0.20
)
```

Possible Pyright Message

Argument of type "Literal['one hundred']"
```

cannot be assigned to parameter "price"
of type "float"
```

Configuration Example

{
```

"typeCheckingMode": "strict",
"include": [
"src"
],
"exclude": [
"tests/generated"
]
}
```

A project configuration file can define checking strictness and which folders should be included or excluded.

```

33.24 Type Hint Best Practices

```

Good type hints should make code easier to understand rather than making it unnecessarily complicated. Public functions, important class attributes, return values, and shared data structures usually benefit most from annotations.

Small local variables often do not need annotations when their type is obvious. Type aliases, protocols, typed dictionaries, and data classes can make complex structures clearer.

Prefer Clear Function Signatures

def calculate_order_total(
prices: list[float],
tax_rate: float
```

) -> float:
subtotal = sum(prices)

```
return subtotal * (
    1 + tax_rate
)

Avoid Unnecessarily Broad Types

# Too broad for many situations
```

def display(value: object) -> None:
print(value)

# More informative

def display_message(
message: str
) -> None:
print(message)
```

Check Optional Values

def get_email(
user_id: int
```

) -> str | None:
users = {
1: "[sara@example.com](mailto:sara@example.com)"
}

```
return users.get(user_id)
```

email = get_email(1)

if email is not None:
print(email.lower())
else:
print("Email not found.")
```

Output

sara@example.com

Use Abstract Collection Types for Inputs

from collections.abc import Iterable
```

def calculate_average(
values: Iterable[float]
) -> float:
collected_values = list(values)

```
return (
    sum(collected_values)
    / len(collected_values)
)
```

print(calculate_average(
[80, 90, 100]
))

print(calculate_average(
(70, 80, 90)
))
```

Output

90.0
```

80.0
```

Recommended Practices

  • Annotate public functions and methods.
  • Annotate return values, including None.
  • Use modern built-in collection syntax when supported.
  • Use narrow and meaningful types.
  • Check optional values before using them.
  • Create type aliases for repeated complex annotations.
  • Use protocols when behavior matters more than inheritance.
  • Use TypedDict for structured dictionaries.
  • Use data classes for structured object records.
  • Run a static type checker regularly.
  • Add strict checking gradually to existing projects.
  • Do not use Any unless it is genuinely necessary.
  • Keep annotations consistent with supported Python versions.
  • Continue validating untrusted data at runtime.
```

33.25 Chapter Practice Exercises

```

Complete these exercises to practise variables, functions, collection annotations, unions, optional values, generic types, protocols, typed dictionaries, static type checking, and modern Python typing syntax.

  1. Annotate variables for a student's name, age, score, and active status.
  2. Create a function that accepts two integers and returns their sum.
  3. Create a function that accepts a string and returns its length.
  4. Annotate a function that returns None.
  5. Create a typed list containing five city names.
  6. Create a typed dictionary mapping product names to prices.
  7. Create a typed set containing course categories.
  8. Create a fixed-length coordinate tuple.
  9. Create a variable-length tuple containing scores.
  10. Create a function that may return a string or None.
  11. Check an optional value before using string methods.
  12. Create a function accepting an integer or string identifier.
  13. Create a union type for integer, float, and string inputs.
  14. Create a literal type for three order statuses.
  15. Create a literal type for ratings from one to five.
  16. Create a type alias for a coordinate.
  17. Create a type alias for a dictionary of student scores.
  18. Create a function that receives another function as a callback.
  19. Create a callable type for a function accepting two floats.
  20. Create a generic box class.
  21. Create a generic stack class.
  22. Create a generic function that returns the last list item.
  23. Create a constrained type variable for integers and floats.
  24. Create a protocol requiring a save() method.
  25. Create two unrelated classes satisfying the same protocol.
  26. Create a typed dictionary for an employee record.
  27. Create optional typed dictionary keys with NotRequired.
  28. Create separate new types for customer and order identifiers.
  29. Create final constants for tax and application name.
  30. Create a class with a shared ClassVar counter.
  31. Create a method returning Self.
  32. Create a class method returning Self.
  33. Rewrite old List syntax using list.
  34. Rewrite old Union syntax using the vertical bar.
  35. Create a program containing three deliberate type errors.
  36. Run MyPy and correct every reported error.
  37. Run a strict type check on a small program.
  38. Create a Pyright configuration file.
  39. Replace unnecessary Any annotations with specific types.
  40. Build a completely typed student report program.

Practice Example: Typed Product Calculator

from typing import Final, TypedDict
```

TAX_RATE: Final[float] = 0.13

class Product(TypedDict):
name: str
price: float
quantity: int

def calculate_subtotal(
products: list[Product]
) -> float:
return sum(
product["price"]
* product["quantity"]
for product in products
)

def calculate_tax(
subtotal: float
) -> float:
return subtotal * TAX_RATE

def display_report(
products: list[Product]
) -> None:
subtotal = calculate_subtotal(
products
)

```
tax = calculate_tax(subtotal)
total = subtotal + tax

print("PRODUCT REPORT")
print("-" * 40)

for product in products:
    item_total = (
        product["price"]
        * product["quantity"]
    )

    print(
        product["name"],
        "-",
        product["quantity"],
        "- $",
        round(item_total, 2)
    )

print("-" * 40)
print(
    "Subtotal:",
    round(subtotal, 2)
)
print(
    "Tax:",
    round(tax, 2)
)
print(
    "Total:",
    round(total, 2)
)
```

products: list[Product] = [
{
"name": "Keyboard",
"price": 49.99,
"quantity": 2
},
{
"name": "Mouse",
"price": 24.99,
"quantity": 1
}
]

display_report(products)
```

Output

PRODUCT REPORT
```

---

Keyboard - 2 - $ 99.98
Mouse - 1 - $ 24.99
-------------------

Subtotal: 124.97
Tax: 16.25
Total: 141.22

33.26 Chapter Mini Project

```

This mini project creates a typed student management system. It demonstrates variable annotations, function annotations, return types, optional values, literal types, type aliases, typed dictionaries, new identifier types, final constants, protocols, generics, class variables, and self types.

Project: Typed Student Management System

from __future__ import annotations
```

from collections.abc import Callable
from dataclasses import dataclass
from typing import (
ClassVar,
Final,
Generic,
Literal,
NewType,
Protocol,
Self,
TypeVar,
TypedDict
)

# Create special identifier types

StudentId = NewType(
"StudentId",
int
)

CourseId = NewType(
"CourseId",
int
)

# Create reusable literal types

Grade = Literal[
"A",
"B",
"C",
"D",
"F"
]

EnrollmentStatus = Literal[
"active",
"completed",
"withdrawn"
]

# Create constants

PASSING_SCORE: Final[float] = 60.0
SCHOOL_NAME: Final[str] = (
"Python Learning Academy"
)

# Create a type alias

ScoreList = list[float]

# Describe a dictionary structure

class StudentSummary(TypedDict):
student_id: StudentId
name: str
average: float
grade: Grade
status: EnrollmentStatus

# Describe printable behavior

class Printable(Protocol):
def display(self) -> None:
...

# Generic result container

ValueType = TypeVar("ValueType")

@dataclass
class Result(Generic[ValueType]):
success: bool
value: ValueType | None = None
error: str | None = None

@dataclass
class Student:
total_students: ClassVar[int] = 0

```
student_id: StudentId
name: str
email: str | None = None
scores: ScoreList | None = None
status: EnrollmentStatus = "active"

def __post_init__(self) -> None:
    Student.total_students += 1

    if self.scores is None:
        self.scores = []

def add_score(
    self,
    score: float
) -> Self:
    if not 0 <= score <= 100:
        raise ValueError(
            "Score must be between 0 and 100."
        )

    # scores is initialized in __post_init__
    assert self.scores is not None

    self.scores.append(score)

    return self

def calculate_average(self) -> float:
    assert self.scores is not None

    if not self.scores:
        return 0.0

    return sum(self.scores) / len(
        self.scores
    )

def calculate_grade(self) -> Grade:
    average = self.calculate_average()

    if average >= 90:
        return "A"

    if average >= 80:
        return "B"

    if average >= 70:
        return "C"

    if average >= 60:
        return "D"

    return "F"

def passed(self) -> bool:
    return (
        self.calculate_average()
        >= PASSING_SCORE
    )

def create_summary(
    self
) -> StudentSummary:
    return {
        "student_id": self.student_id,
        "name": self.name,
        "average": round(
            self.calculate_average(),
            2
        ),
        "grade": self.calculate_grade(),
        "status": self.status
    }

def display(self) -> None:
    email_display = (
        self.email
        if self.email is not None
        else "No email"
    )

    print(
        f"{self.student_id:<5} "
        f"{self.name:<16} "
        f"{self.calculate_average():>7.2f} "
        f"{self.calculate_grade():>5} "
        f"{self.status:>10} "
        f"{email_display}"
    )
```

class StudentRepository:
def **init**(self) -> None:
self._students: dict[
StudentId,
Student
] = {}

```
def add(
    self,
    student: Student
) -> Result[Student]:
    if student.student_id in self._students:
        return Result(
            success=False,
            error=(
                "A student with this "
                "identifier already exists."
            )
        )

    self._students[
        student.student_id
    ] = student

    return Result(
        success=True,
        value=student
    )

def find(
    self,
    student_id: StudentId
) -> Student | None:
    return self._students.get(
        student_id
    )

def all_students(
    self
) -> list[Student]:
    return list(
        self._students.values()
    )

def filter_students(
    self,
    condition: Callable[
        [Student],
        bool
    ]
) -> list[Student]:
    return [
        student
        for student in self._students.values()
        if condition(student)
    ]
```

def display_printable(
item: Printable
) -> None:
item.display()

def create_sample_students() -> list[Student]:
first_student = (
Student(
student_id=StudentId(101),
name="Sara",
email="[sara@example.com](mailto:sara@example.com)"
)
.add_score(92)
.add_score(95)
.add_score(90)
)

```
second_student = (
    Student(
        student_id=StudentId(102),
        name="Michael"
    )
    .add_score(81)
    .add_score(84)
    .add_score(79)
)

third_student = (
    Student(
        student_id=StudentId(103),
        name="Ali",
        email="ali@example.com"
    )
    .add_score(55)
    .add_score(68)
    .add_score(60)
)

fourth_student = (
    Student(
        student_id=StudentId(104),
        name="Emma",
        status="completed"
    )
    .add_score(88)
    .add_score(91)
    .add_score(86)
)

return [
    first_student,
    second_student,
    third_student,
    fourth_student
]
```

def display_report(
repository: StudentRepository
) -> None:
students = repository.all_students()

```
print(SCHOOL_NAME)
print("=" * 85)

print(
    f"{'ID':<5} "
    f"{'Name':<16} "
    f"{'Average':>7} "
    f"{'Grade':>5} "
    f"{'Status':>10} "
    f"Email"
)

print("-" * 85)

for student in sorted(
    students,
    key=lambda item: (
        item.calculate_average()
    ),
    reverse=True
):
    display_printable(student)

print("-" * 85)

passing_students = (
    repository.filter_students(
        lambda student: student.passed()
    )
)

active_students = (
    repository.filter_students(
        lambda student: (
            student.status == "active"
        )
    )
)

print(
    "Total students:",
    len(students)
)

print(
    "Passing students:",
    len(passing_students)
)

print(
    "Active students:",
    len(active_students)
)

print(
    "Objects created:",
    Student.total_students
)
```

def main() -> None:
repository = StudentRepository()

```
students = create_sample_students()

for student in students:
    result = repository.add(student)

    if not result.success:
        print(
            "Error:",
            result.error
        )

display_report(repository)

print()
print("SEARCH RESULT")
print("-" * 40)

student = repository.find(
    StudentId(102)
)

if student is not None:
    summary = student.create_summary()

    print(
        "Name:",
        summary["name"]
    )

    print(
        "Average:",
        summary["average"]
    )

    print(
        "Grade:",
        summary["grade"]
    )
else:
    print("Student was not found.")
```

if **name** == "**main**":
main()
```

Output

Python Learning Academy
```

=====================================================================================
ID    Name             Average Grade     Status Email
-----------------------------------------------------

101   Sara               92.33     A     active [sara@example.com](mailto:sara@example.com)
104   Emma               88.33     B  completed No email
102   Michael            81.33     B     active No email
103   Ali                61.00     D     active [ali@example.com](mailto:ali@example.com)
-----------------------------------------------------------------------------------------

Total students: 4
Passing students: 4
Active students: 3
Objects created: 4

## SEARCH RESULT

Name: Michael
Average: 81.33
Grade: B
```

Project Explanation

NewType creates separate student and course identifier types. They remain integers at runtime, but static type checkers can distinguish their meanings.

Literal limits grades and enrollment statuses to predefined values. This helps prevent spelling mistakes such as using "actve" instead of "active".

Final marks the passing score and school name as values that should not be reassigned. ClassVar marks the student counter as shared class data.

The generic Result class stores either a successful value or an error message. The specific value type is preserved through its generic parameter.

The Printable protocol allows the display function to accept any object with a compatible display() method. The object does not need to inherit from the protocol.

The repository uses a typed dictionary to map student identifiers to student objects. Its find() method returns either a student or None, requiring the caller to check whether a result exists.

The Self return type allows the add_score() method to return the same student object. This enables method chaining when sample students are created.

How to Run the Project

  1. Create a file named typed_student_manager.py.
  2. Copy the complete project into the file.
  3. Save the file.
  4. Open a terminal in the same folder.
  5. Run python typed_student_manager.py.
  6. On some computers, run python3 typed_student_manager.py.
  7. Install MyPy with python -m pip install mypy.
  8. Run python -m mypy typed_student_manager.py.
  9. Correct any type issues reported by your installed checker.

Mini Project Challenges

  • Add a typed course class.
  • Use CourseId to identify courses.
  • Create a typed enrollment dictionary.
  • Add a method to remove a student.
  • Add an optional phone number.
  • Create a literal type for attendance status.
  • Create a typed dictionary for course summaries.
  • Create a generic repository that supports several model types.
  • Create a protocol for objects that can be saved.
  • Create CSV and JSON exporter classes.
  • Add a callback for custom student filtering.
  • Create a final maximum class size.
  • Prevent adding students after the maximum is reached.
  • Run MyPy in strict mode.
  • Create a Pyright configuration file for the project.
```
Chapter 2: Development EnvironmentPrepare Your JavaScript Workspace

A modern course built to help learners study step by step with clarity, comfort, and confidence.