Learn
06.5 | TYPE HINTS AND DOCUMENTATION
01 | MAKE EXPECTATIONS VISIBLE
A function called find_book receives an identifier and may return a Book. Without a clear signature, a caller must inspect the implementation to discover what to pass and whether absence is possible. Type hints make those expectations visible.
In def double(number: int) -> int, the parameter annotation describes the intended input type and the arrow describes the intended return type. A type checker can analyse source and report likely inconsistencies without executing every route.
Python does not normally enforce these hints at runtime. An annotation is not an automatic converter, a range check or an exception handler. Keep those responsibilities separate.
def double(number: int) -> int:
return number * 2
print(double(6))EXPECTED OUTPUT
12
02 | HINT COLLECTION CONTENTS
list[str] describes a list intended to hold strings. dict[str, int] describes string keys with integer values. tuple[str, int] describes a fixed two-item tuple, while tuple[str, ...] describes a variable-length tuple of strings.
These are declarations about expected use, not runtime filters. Appending an integer to a variable annotated list[str] can still happen in ordinary Python; a checker may report the mismatch.
The examples use Python 3.10 or later so built-in generic syntax and union syntax fit the same baseline. Keep the project’s required version in its handover instructions.
names: list[str] = ["Maya", "Noor"]
scores: dict[str, int] = {"Maya": 8}
result: tuple[str, int] = ("Maya", 8)
print(names[0], scores["Maya"], result[1])EXPECTED OUTPUT
Maya 8 8
03 | DESCRIBE ABSENCE AND NO RESULT
int | None means an integer or None. A search returning an index may need this annotation because zero is a valid index and None means absent. A caller must handle both possibilities.
A method annotated -> None performs its job without supplying a meaningful result; it still returns the actual value None. It may have side effects such as adding a member or printing output, which a type hint alone does not describe.
Use narrowing: after if position is not None, code can treat the value as an integer. Do not use truthiness to distinguish absence from index zero.
def find_first(values: list[int], target: int) -> int | None:
for index, value in enumerate(values):
if value == target:
return index
return None
position = find_first([8, 5], 8)
if position is not None:
print("Found at:", position)EXPECTED OUTPUT
Found at: 0
04 | ANNOTATE METHODS WITHOUT DUPLICATING SELF
A normal instance method still has self as its first parameter. Annotate explicit arguments and the returned result. Type checkers generally infer the instance type for self inside that class.
__init__ is annotated -> None because it initialises the instance rather than returning the new object. A method returning a Loan can name that class as its result.
An attribute annotation documents expected state. An annotation without an assignment does not necessarily create an instance value that a later method can read; initialise required attributes deliberately.
class Counter:
def __init__(self, start: int = 0) -> None:
self._value: int = start
def increment(self) -> int:
self._value += 1
return self._value
print(Counter(4).increment())EXPECTED OUTPUT
5
05 | STATIC REPORTS AND RUNTIME CHECKS
A static type checker examines source against annotations and its configured rules. It can catch a call supplying text where an integer is expected. It cannot independently prove that every user expense is genuine or that a numeric value lies in your required range.
Runtime validation examines actual values when the program executes. The library explicitly checks copy IDs, date objects and policy numbers even though those inputs are annotated.
The deliberate mismatch below still runs because returning value does not itself require a string operation. A checker should flag the call, while runtime Python returns the integer. This example demonstrates the limit; it is not recommended application use.
def echo_label(value: str) -> str:
return value
# Deliberate mismatch to demonstrate that hints are not runtime enforcement.
result = echo_label(7)
print(result)
print(type(result).__name__)EXPECTED OUTPUT
7 int
06 | DOCSTRINGS EXPLAIN WHAT TYPES CANNOT
A docstring is the first string statement in a class, function or method body. It becomes documentation accessible through help() and the object’s __doc__. Use it to describe the job and important rules.
A useful contract says what inputs mean, what is returned, what state changes and which failures are deliberately raised. date alone does not tell a caller whether the due day itself counts as overdue.
Avoid repeating only the signature in prose. Explain that return_book removes an active loan, rejects a date before borrowing and returns the removed record. This communicates behaviour that annotations cannot express.
def is_overdue(due_day, check_day) -> bool:
"""Return whether check_day is after due_day; due day itself is not late."""
return check_day > due_day
print(is_overdue.__doc__)EXPECTED OUTPUT
Return whether check_day is after due_day; due day itself is not late.
07 | COMMENTS EXPLAIN DECISIONS
Comments should explain a reason, assumption or non-obvious choice. A comment saying add one above count += 1 repeats the code. A comment explaining that bool must not be accepted as a policy integer records a useful design reason.
Keep comments and docstrings accurate after changes. Outdated documentation can be more misleading than none because a reader may trust a rule the code no longer implements.
Document the project entry point, required Python version, commands, data lifetime and how to run tests. A class signature alone cannot provide that complete handover.
08 | A PROTOCOL CAN DESCRIBE BEHAVIOUR
A Protocol from typing can describe an expected interface without requiring every implementation to inherit from one shared custom base. A sensor interface might require read() -> int. This formalises the duck-typing idea from 06.3 for static analysis.
Protocol definitions and runtime checks are different matters. This introduction does not require them in the project. A small clear method contract may be enough while you are learning.
Do not add Any simply to silence every report. Any relaxes type checking, so use it intentionally when information is truly dynamic. Clear interfaces provide more useful feedback than annotations added only for appearance.
09 | SEPARATE TYPE AND VALUE RULES
Select a runtime value for a function documented as accepting an integer quantity from zero through ten. The model reports the type expectation separately from the application range rule. It does not run a static checker.
10 | USE THREE COMPLEMENTARY CHECKS
Type checking analyses declared interfaces. Runtime validation enforces rules on actual values. Tests compare selected behaviours with independent expectations. These tools overlap but none replaces the others.
For the library, annotate Book, Member and Loan records and service methods. Validate external boundaries. Test duplicate IDs, due-date boundaries and rejected state transitions. Document session-only storage.
A well-documented class is one a caller can use correctly without reading every private line, while still knowing the limits of the promises being made.
PUT YOUR SKILLS TO WORK
Use the Section 6 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 6 challenges Python course navigation