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.
Main reading content
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.
Chapter 33 Topics
```- 33.1 Dynamic Typing Review
- 33.2 Why Type Hints Are Used
- 33.3 Variable Annotations
- 33.4 Function Annotations
- 33.5 Return Types
- 33.6 Collection Types
- 33.7 Optional Types
- 33.8 Union Types
- 33.9 Literal Types
- 33.10 Type Aliases
- 33.11 Callable Types
- 33.12 Generic Types
- 33.13 Type Variables
- 33.14 Protocols
- 33.15 TypedDict
- 33.16 NewType
- 33.17 Final
- 33.18 ClassVar
- 33.19 Self Types
- 33.20 Modern Python Type Syntax
- 33.21 Static Type Checkers
- 33.22 MyPy
- 33.23 Pyright
- 33.24 Type Hint Best Practices
- 33.25 Chapter Practice Exercises
- 33.26 Chapter Mini Project
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
Nonewithout 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
TypedDictfor 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
Anyunless 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.
- Annotate variables for a student's name, age, score, and active status.
- Create a function that accepts two integers and returns their sum.
- Create a function that accepts a string and returns its length.
- Annotate a function that returns
None. - Create a typed list containing five city names.
- Create a typed dictionary mapping product names to prices.
- Create a typed set containing course categories.
- Create a fixed-length coordinate tuple.
- Create a variable-length tuple containing scores.
- Create a function that may return a string or
None. - Check an optional value before using string methods.
- Create a function accepting an integer or string identifier.
- Create a union type for integer, float, and string inputs.
- Create a literal type for three order statuses.
- Create a literal type for ratings from one to five.
- Create a type alias for a coordinate.
- Create a type alias for a dictionary of student scores.
- Create a function that receives another function as a callback.
- Create a callable type for a function accepting two floats.
- Create a generic box class.
- Create a generic stack class.
- Create a generic function that returns the last list item.
- Create a constrained type variable for integers and floats.
- Create a protocol requiring a
save()method. - Create two unrelated classes satisfying the same protocol.
- Create a typed dictionary for an employee record.
- Create optional typed dictionary keys with
NotRequired. - Create separate new types for customer and order identifiers.
- Create final constants for tax and application name.
- Create a class with a shared
ClassVarcounter. - Create a method returning
Self. - Create a class method returning
Self. - Rewrite old
Listsyntax usinglist. - Rewrite old
Unionsyntax using the vertical bar. - Create a program containing three deliberate type errors.
- Run MyPy and correct every reported error.
- Run a strict type check on a small program.
- Create a Pyright configuration file.
- Replace unnecessary
Anyannotations with specific types. - 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
- Create a file named
typed_student_manager.py. - Copy the complete project into the file.
- Save the file.
- Open a terminal in the same folder.
- Run
python typed_student_manager.py. - On some computers, run
python3 typed_student_manager.py. - Install MyPy with
python -m pip install mypy. - Run
python -m mypy typed_student_manager.py. - Correct any type issues reported by your installed checker.
Mini Project Challenges
- Add a typed course class.
- Use
CourseIdto 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.
A modern course built to help learners study step by step with clarity, comfort, and confidence.