OSPython.023: Type Hints and Annotations Basics

Diagram showing Python type annotations flowing into a static type checker that catches a mismatch before runtime.

Type hints let you describe what kind of data your Python code expects without changing Python into a statically typed language.

This lesson follows OSPython.022: Context Managers and the with Statement Basics. We are continuing the recent run of Python language features by adding something that makes larger programs easier to read, debug, and maintain: type annotations.

Start with the smallest example

age: int = 25
name: str = "Ada"

Read those lines in plain English:

  • age: int says that age is intended to hold an integer.
  • name: str says that name is intended to hold a string.

The annotation appears after the variable name and a colon.

variable_name: expected_type = value

The most important rule: a hint is not a runtime lock

Python does not normally stop this assignment just because the annotation says int:

age: int = 25
age = "twenty-five"

The program may still run because annotations are primarily metadata for humans, editors, linters, and static type-checking tools. A checker can warn that a string is being assigned where an integer was expected.

Your code
   ↓
Type annotations
   ↓
Editor / static type checker
   ↓
Possible mismatch warning before execution

Python’s official typing documentation explains that the runtime does not enforce function and variable type annotations. Tools such as type checkers, IDEs, and linters can use them instead.

Video 1: Type hints and annotations from the ground up

Tech With Tim — Python Typing: Type Hints & Annotations. Covers variables, function annotations, collections, optional values, callable types, and static analysis.

Function parameter annotations

Type hints become especially useful on functions because they tell the reader what should go in and what should come back out.

def double_hashrate(hashrate: float) -> float:
    return hashrate * 2

Break it apart:

  • hashrate: float means the parameter is expected to be a floating-point number.
  • -> float means the function is expected to return a floating-point number.
input type                 return type
    ↓                           ↓
def double_hashrate(hashrate: float) -> float:
    return hashrate * 2

More basic parameter examples

def greet(name: str) -> str:
    return f"Hello, {name}"


def is_online(status_code: int) -> bool:
    return status_code == 200


def log_message(message: str) -> None:
    print(message)

-> None is useful when a function performs an action but is not intended to return a meaningful value.

Collections can be annotated too

Modern Python syntax can describe the type of the collection and the type stored inside it.

miners: list[str] = ["S21", "S19", "A1566"]

power_draws: list[int] = [3500, 3250, 3420]

rack_power: dict[str, int] = {
    "rack-a": 12000,
    "rack-b": 11500,
}

Read them as:

  • list[str] — a list whose items are strings.
  • list[int] — a list whose items are integers.
  • dict[str, int] — a dictionary with string keys and integer values.

Tuples and sets

coordinates: tuple[int, int] = (10, 25)

active_ips: set[str] = {
    "10.0.0.10",
    "10.0.0.11",
}

The same idea applies: the outer name tells you the container, and the type inside the brackets tells you what the container is expected to hold.

Video 2: Basic annotations through advanced typing

Corey Schafer — Python Type Hints: From Basic Annotations to Advanced Generics. This gives a broader view of how type hints scale from simple variables and functions into larger projects.

One value can sometimes have more than one valid type

Suppose a function accepts either an integer ID or a string ID. You can describe that with a union:

def find_device(device_id: int | str) -> str:
    return f"Searching for {device_id}"

int | str means integer or string.

device_id
   │
   ├── int
   └── str

Older Python code may express the same idea with Union[int, str] from the typing module. When maintaining existing code, you may see both styles.

Optional values and None

A value that may be a string or may be absent can be written as:

serial_number: str | None = None

Later the program may assign a real serial number:

serial_number = "ABC12345"

Again, older code may use Optional[str]. Conceptually, both describe a value that can be a string or None.

Type aliases make long annotations easier to read

If the same complicated type appears repeatedly, give it a useful name.

PowerReading = tuple[str, float]


def read_power() -> PowerReading:
    return ("rack-a", 11.8)

The name PowerReading communicates intent better than repeating the entire structure everywhere.

What a static type checker can catch

Imagine this function:

def calculate_kw(amps: float, volts: float) -> float:
    return amps * volts / 1000

Then somebody accidentally calls it like this:

calculate_kw("40 amps", 240.0)

The first argument is a string even though the function says it expects a float. Python itself may only complain once the incompatible operation occurs at runtime, but a static checker can often flag the mismatch earlier.

That is the value of type hints in larger systems: they turn some mistakes into visible warnings before a production path executes them.

Video 3: Static checking with mypy

Real Python — Type-Checking Python Programs With Type Hints and mypy. This demonstrates how a separate static checker can use annotations to find potential type mistakes.

Type hints also improve editor assistance

Editors and language servers can use annotations to understand what an object is expected to be. That can improve autocomplete, parameter hints, navigation, and warnings.

def efficiency(hashrate_th: float, watts: float) -> float:
    return watts / hashrate_th

A reader can immediately see that both inputs and the output are numeric, even before reading the function body.

Annotations are documentation that stays close to the code

Compare these two functions:

def update_device(device, online):
    ...


def update_device(device: str, online: bool) -> None:
    ...

The second version gives you useful information immediately:

  • device should be a string.
  • online should be a Boolean.
  • The function is not intended to return a meaningful value.

Do not annotate everything just because you can

Type hints should make code clearer, not harder to read. This is useful:

def get_temperature(sensor_id: str) -> float:
    ...

An enormous deeply nested annotation can become harder to understand than the code itself. When a type gets complicated, consider a type alias, a named class, or another clearer design.

A practical data-center example

def rack_status(
    rack_name: str,
    power_kw: float,
    temperatures: list[float],
    alarm: str | None = None,
) -> dict[str, object]:
    return {
        "rack": rack_name,
        "power_kw": power_kw,
        "temperatures": temperatures,
        "alarm": alarm,
    }

Before reading the body, you already know the expected shape of the inputs. That is valuable when multiple technicians, engineers, or automated systems touch the same codebase.

Annotations can be inspected

Function annotations are available to Python as metadata. For example:

def add(a: int, b: int) -> int:
    return a + b

print(add.__annotations__)

You do not need to use this often as a beginner. It simply proves an important idea: annotations are information attached to the function.

Where the typing module fits

Built-in syntax handles many common cases now, but the typing module provides additional tools for describing more complex interfaces and data structures.

Python’s original standardized type-hinting design is documented in PEP 484. You do not need to memorize the PEP; it is useful as a reference for understanding where modern Python type hints came from.

Common beginner mistakes

  • Thinking a type hint automatically prevents the wrong value at runtime.
  • Assuming annotations make Python behave like Java or C++ at runtime.
  • Using a very broad type such as object everywhere and losing useful information.
  • Writing annotations so complicated that nobody can quickly understand them.
  • Forgetting that None must be included when a value can legitimately be absent.
  • Confusing a type checker warning with a Python runtime exception.

Quick practice

  1. Annotate a variable named hostname as a string.
  2. Annotate a variable named port as an integer.
  3. Write a function that accepts two floats and returns a float.
  4. Create a list[str] containing three server names.
  5. Create a dict[str, int] mapping rack names to power readings.
  6. Write one value that can be either a string or None.
  7. Explain why a type hint does not automatically enforce the type at runtime.

Knowledge check

1. What does name: str mean?
It says that name is intended to contain a string.

2. What does -> bool mean on a function?
The function is expected to return a Boolean value.

3. Does Python automatically reject every value that violates a type hint?
No. Type hints are not normally runtime enforcement.

4. What does str | None mean?
The value may be a string or None.

5. Why use a static type checker?
It can analyze annotations and warn about many mismatched types before the affected code runs.

Key takeaway

Type hints describe the data your Python code expects. They improve readability and give editors and static analysis tools more information, but they do not normally enforce types at runtime.

Remember the visual:

Python code
   ↓
Type annotations describe intent
   ↓
Humans + editors + type checkers understand more
   ↓
Many mistakes become easier to spot earlier

Display note: all examples in this lesson are plain code blocks and educational diagrams. They are not simulated VS Code, Windows, or Linux terminals, so no terminal color palette has been invented or represented.

Leave a comment