Skip to content
Python Basics: Describing Data with Type Hints

Python Basics: Describing Data with Type Hints

October 13, 2026

Type hints describe the values a function expects and returns. They help readers and development tools follow your code. Python itself does not enforce those hints when the program runs.

Before you start

These three independent scripts were tested on Linux with Bash, Python 3.12.14 and Python 3.13.5. They need no extra packages or environment variables. Commands use python3; substitute python3.13 for the other tested interpreter.

The examples demonstrate runtime behavior. We do not install or run a separate static type checker here. Deliberate type mismatches are labelled below; a type checker can report them even when Python executes the code.

Create a fresh working directory and keep this Bash session open:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each UTF-8 source block in this directory using its stated filename. Avoid typing.py, which would shadow the standard-library module. The scripts only print results and do not write data files or access the network.

Annotate a function

Save as demo_typing_greeting.py:

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


print(greet("Ada"))
print(greet("Lin", prefix="Welcome"))
print(greet(42))  # Intentional type mismatch: name is meant to be str.

Run it:

python3 demo_typing_greeting.py

Output:

Hello, Ada!
Welcome, Lin!
Hello, 42!

name: str describes a parameter, while -> str describes the return value. The default "Hello" lets callers omit prefix. These basic hints use built-in types, so no import is needed. See function annotations.

The last call deliberately passes an integer. String formatting accepts it, so the program still runs. The annotation neither rejects the value nor converts it into a string before the function starts. Python leaves annotation checking to separate tools. See the typing module’s runtime note.

Describe containers and missing results

Save as demo_typing_word_counts.py:

def count_words(words: list[str]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for word in words:
        normalized = word.strip().lower()
        if normalized:
            counts[normalized] = counts.get(normalized, 0) + 1
    return counts


def lookup_count(counts: dict[str, int], word: str) -> int | None:
    return counts.get(word)


counts = count_words(["Tea", "coffee", " tea ", " "])
print("Counts:", counts)
print("Empty input:", count_words([]))
print("Tea:", lookup_count(counts, "tea"))
print("Juice:", lookup_count(counts, "juice"))

Run it:

python3 demo_typing_word_counts.py

Output:

Counts: {'tea': 2, 'coffee': 1}
Empty input: {}
Tea: 2
Juice: None

list[str] describes a list of strings. dict[str, int] describes string keys and integer values. The annotation on counts also documents an initially empty dictionary. This built-in container syntax needs no typing.List or typing.Dict import. See generic container types.

int | None allows either an integer or None. Here a missing word produces None; it is different from an existing word with a count of zero. A result that may be None needs handling before arithmetic. See union types.

The code trims and lowercases words while counting them. Lookup expects an already normalized word. These behavior choices come from the function bodies; annotations do not trim text, reject blank words, or validate list contents.

Describe named dictionary fields

Save as demo_typing_book_record.py:

from typing import NotRequired, TypedDict


class Book(TypedDict):
    title: str
    pages: int
    author: NotRequired[str]


def describe(book: Book) -> str:
    author = book.get("author", "unknown")
    return f"{book['title']} ({book['pages']} pages) by {author}"


book: Book = {"title": "Garden Notes", "pages": 80, "author": "A. Green"}
anonymous: Book = {"title": "Trail Guide", "pages": 24}
print(describe(book))
print(describe(anonymous))
print("Runtime type:", type(book).__name__)

incomplete: Book = {"title": "Draft"}  # Intentional missing required key.
print("Incomplete dictionary created:", incomplete)
try:
    describe(incomplete)
except KeyError as error:
    print("Expected missing key:", error.args[0])
else:
    raise AssertionError("Reading the missing pages key should fail")

Run it:

python3 demo_typing_book_record.py

Output:

Garden Notes (80 pages) by A. Green
Trail Guide (24 pages) by unknown
Runtime type: dict
Incomplete dictionary created: {'title': 'Draft'}
Expected missing key: pages

TypedDict gives individual keys their own expected types. Keys are required unless marked otherwise. NotRequired[str] permits an absent key; it does not permit None as its value. At runtime these records are ordinary dictionaries. See TypedDict.

The incomplete dictionary is created successfully. Its missing key triggers a normal KeyError only when describe reads pages. The try block catches that intentional failure so the script finishes normally. A required type hint alone cannot protect data arriving from JSON, forms, or other external sources. Validate those values before relying on them.

Clean up

In the same Bash session, remove the scripts and empty working directory:

rm -- demo_typing_greeting.py demo_typing_word_counts.py demo_typing_book_record.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

References

Acknowledgements

This post was created with assistance from AI.

Last updated on