Skip to content
beginner

Python Type Hints: Annotate Function Parameters and Return Values

Your function works perfectly until someone hands it a string where you expected a number. Then it fails three lines deep, far from the call that caused…

Published 2026-10-02Updated 2026-10-049 min read
A scenic view of Panama City skyline captured from the waterfront, highlighting modern skyscrapers.
A scenic view of Panama City skyline captured from the waterfront, highlighting modern skyscrapers. Photo by Moo Lens on Pexels.

Your function works perfectly until someone hands it a string where you expected a number. Then it fails three lines deep, far from the call that caused the problem. Type hints are how you write down what a function expects and what it gives back — and they are also how beginners get fooled, because Python reads them and then ignores them at runtime.

Let's fix the mental model first, then prove it with code you can run.

What Type Hints Actually Do

A two-column comparison shows the Python interpreter storing annotations and running without checking types, while an editor or type checker reads the same annotations and can flag a mismatch before execution.
Type hints describe expected types; Python runs the code, while development tools can check those expectations.

A type hint is an annotation: extra information attached to a variable, a parameter, or a return value. That is the whole mechanism. Python stores the annotation, makes it readable, and moves on.

The interpreter does not check annotations when your function runs. If you come from a language like Java or C++, this is the part that surprises you. In those languages the compiler refuses to build the program when types don't line up. Python will happily run your code, wrong types and all.

So where do hints earn their keep? In the tools that read them: your editor, linters, and static type checkers. A static type checker is a program that reads your source code without running it and reports places where the types don't add up. That's the audience for your annotations.

Keep that split in mind — Python runs the code, tools read the hints — and everything else in this article follows from it.

Knowledge check

Check your understanding

Answer this question before you continue.

Which statement correctly describes what happens when Python runs a function with type hints?
Misconception Check

Focus: Distinguish Python's handling of annotations from the checks performed by development tools.

Your First Annotated Function

Here's a small function with annotated parameters and an annotated return value. Save it as greet.py:

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

print(greet("Ada", 2))

Run it:

python greet.py

Expected output:

Hello, Ada! Hello, Ada! 

Now the interesting part. Call it with the wrong type:

print(greet("Ada", "2"))

Expected output:

Hello, Ada! 

No error. No warning. The annotation said times: int, you passed a string, and Python shrugged. The string "2" multiplied by a string just repeats it once, so you get a single greeting — a silently wrong result instead of a crash.

Read the syntax in pieces:

PieceMeaning
name: strAnnotates the parameter name as a string
times: intAnnotates the parameter times as an integer
-> strAnnotates the return value as a string
times: int = 2Sets a default value — a different thing entirely

That last row matters. The colon annotates; the equals sign assigns a default. def greet(name: str, times: int = 1) means "expect an int, and use 1 if nobody passes one." Mixing those two up is the first mistake most beginners make.

Knowledge check

Check your understanding

Answer this question before you continue.

Using the article's `greet` function, what does `print(greet("Ada", "2"))` display?
Output Prediction

Focus: Predict the result of calling an annotated function with a string where an integer was expected.

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

Annotating Built-in and Collection Types

The simple built-ins cover most of what you'll write: int, float, str, bool, and bytes.

Collections need one extra idea. When a function takes a list, you usually want to say what's inside the list, not just that it's a list. You do that with square brackets:

def total(prices: list[float]) -> float:
    return sum(prices)

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

print(total([1.5, 2.25, 3.0]))
print(word_counts(["red", "blue", "red"]))

Expected output:

6.75
{'red': 2, 'blue': 1}

The brackets describe the contents, not the container. list[float] means "a list whose elements are floats." dict[str, int] means "a dictionary with string keys and integer values." tuple[str, int] means a two-element tuple — a string followed by an int. set[int] means a set of integers.

Note: Older code and older Python versions write these as List, Dict, Tuple, and Set, imported from the typing module. Both forms appear in real projects, so you should be able to read either one. The lowercase bracket form is the modern style.

Two more cases you'll hit quickly. A function that returns nothing gets -> None:

def log(message: str) -> None:
    print(f"[log] {message}")

And a value that might be missing needs a union — a type that says "one of these":

def find_user(user_id: int) -> str | None:
    if user_id == 1:
        return "Ada"
    return None

str | None reads as "a string, or None." Older code writes this as Optional[str] from typing. Same meaning.

Knowledge check

Check your understanding

Answer this question before you continue.

What does `dict[str, int]` describe in the article's collection-annotation examples?
Single Choice

Focus: Interpret element and key/value types in collection annotations.

Annotating Variables, Not Just Functions

The colon syntax works on plain variables too:

age: int = 25
name: str = "Ada"
scores: list[int] = [90, 85, 77]

Here's the part to internalize: the annotation is optional, and the assignment is what actually creates the value. Annotating a variable does not lock its type. This is perfectly legal:

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

Python won't complain. Your editor might underline it, and a type checker will flag it, but the interpreter doesn't care. A variable annotation is a note to your tools and your future self, not a declaration the language enforces.

Most of the value lives in function signatures, so that's where I'd spend your annotation budget first.

Knowledge check

Check your understanding

Answer this question before you continue.

After `age: int = 25`, what happens if the code assigns `age = "twenty-five"`?
Misconception Check

Focus: Explain that a variable annotation does not prevent later assignment of a different type.

What Python Does Not Enforce

You've already seen the interpreter ignore a wrong-typed argument. The useful follow-up is where the annotations go. Every function exposes them through __annotations__:

def double(value: int) -> int:
    return value * 2

print(double.__annotations__)

Expected output:

{'value': <class 'int'>, 'return': <class 'int'>}

That dictionary is the whole point. The annotations sit on the function as data, which is exactly why editors and type checkers can read them and Python's runtime doesn't have to.

When errors do appear in annotated code, they come from the operation inside the function, not from the hint. If double had called value + 1 instead, you'd get a real TypeError — but from the addition, not from the annotation.

Common mistake: Passing the wrong type, seeing no error, and concluding type hints are broken. They aren't broken — they were never a runtime check. The check happens in your tools, before you run anything.

Where Type Hints Pay Off

The honest answer is: not everywhere, and not immediately. But in specific situations, hints change how much work a change costs you.

Your editor uses hints to offer accurate autocomplete and to flag mismatches as you type. A static type checker like mypy reads the same hints and reports inconsistencies before the program runs — think of it as a very thorough linter. And hints act as documentation that lives next to the code, which makes it harder for them to drift out of date than a comment sitting above a function.

The payoff shows up when a script grows. A 20-line script that cleans a CSV file is fine without hints. The same script after three months, with six functions calling each other, is a different animal. When you change what one function returns, hints tell you which callers now expect the wrong thing — before you run the program and watch it fail somewhere unrelated.

I've watched this pattern enough times to trust it: beginners who annotate the functions they keep end up changing those functions with far less fear. The hints turn "I hope nothing else used this" into "the checker already told me what used this."

When to Skip Type Hints

Hints have a cost: keystrokes, visual noise, and a small amount of maintenance. For a five-line throwaway script, that cost buys you almost nothing.

SituationAnnotate?Why
Scratch file you'll delete todayNoThe cost outweighs the benefit
Quick experiment to test an ideaNoYou're exploring, not maintaining
Function called from more than one placeYesCallers need the contract
Code you'll revisit next weekYesYou are a future stranger to yourself
Shared with a teammateYesHints document intent without a meeting

A good beginner instinct: annotate the functions you keep, skip the ones you throw away. Start with signatures — parameters and return values — because that's where each annotation carries the most information.

Common Beginner Mistakes

Confusing : with =. def f(x: int) annotates. def f(x: int = 5) annotates and sets a default. They look similar and do different jobs.

Expecting a runtime error. Covered above, but it's the mistake that generates the most confusion. Hints don't police behavior.

Using list[int] on an old Python. Subscripted built-ins like list[int] need Python 3.9 or newer. On 3.8 and earlier you'll get a TypeError at definition time. If you're stuck on an older version, import List from typing and write List[int].

Annotating the return type as the input type. A function that takes a list and returns a count returns int, not list. Read your return statement and annotate what actually comes out.

Forgetting -> None. Functions that print instead of returning still need a return annotation. None is the honest answer.

Copying from __future__ import annotations without knowing why. Older code uses it to make annotations lazy so forward references work. You don't need it in a beginner script.

Practice: Annotate and Break Your Own Function

Take a function you already wrote — anything with at least one parameter and a return value. Then:

  1. Add parameter and return annotations.
  2. Run it and confirm the output is identical to before.
  3. Call it with a deliberately wrong-typed argument. Record what happens: an error, or a silently wrong result?
  4. Print your_function.__annotations__ and read the dictionary.

For the extension, install a static type checker and run it on the same file:

pip install mypy
mypy your_file.py

Compare what mypy reports against what Python did when you ran the file. That gap — the checker sees the problem, the interpreter doesn't — is the entire lesson in one command.

Then try one more: annotate a function that takes a list[str] and returns a dict[str, int], using the collection syntax from earlier.

The rule to carry forward is short. Annotations describe intent. Python doesn't enforce them. Tools do. Annotate the next function you write, run it, then pass the wrong type on purpose and watch nothing stop you. Once that's comfortable, running a type checker on your own code is the natural next step — and it's the moment hints stop being decoration and start catching real mistakes.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

A small script has grown to several functions that call one another. What benefit of type hints does the article emphasize when one function's return value changes?
Question 1 of 2Single Choice

Focus: Identify when type hints can help manage changes in code with multiple function callers.

In the article's suggested practice, what should you expect when you run a function with a deliberately wrong-typed argument and then check the file with mypy?
Question 2 of 2Misconception Check

Focus: Contrast what Python does when running incorrectly typed calls with what a static type checker can report.

References

  1. typing — Support for type hints — Python 3.14.8 documentationdocs.python.org
  2. PEP 484 – Type Hints - Python Enhancement Proposalspeps.python.org
Practical resource

Want a more structured Python path?

Use the Python Starter Pack to turn scattered tutorials into a focused practice path.

View the bundle
Coming soon

Python for Artificial Intelligence Starter Pack

Build a Python foundation you can actually use. The Python for AI Starter Pack brings together a guided path through setup, core programming concepts, data structures, files, JSON, APIs, debugging, and practical projects—so you can move quickly from running your first program to understanding and building useful software.

$9
PDF BundlePythonAIBeginner
  • 264-page illustrated PDF
  • 12 guided Python chapters
  • Visual concept diagrams
  • Self-assessment quizzes
  • Bonus deep-dive sections
  • Files, JSON, APIs, debugging & projects
  • Foundation for data, automation & AI

Coming soon

Free Python bundle

Get the LearnPyFast Python for Artificial Intelligence Starter Bundle

Build a Python foundation you can actually use. The Python for Artificial Intelligence Starter Pack brings together a guided path through setup, core programming concepts, data structures, files, JSON, APIs, debugging, and practical projects—so you can move quickly from running your first program to understanding and building useful software.

You’ll receive the bundle by email. You can unsubscribe anytime.

No spam. You can unsubscribe anytime. See our Privacy policy.

Related sites

Continue beyond Python

Explore related Worldmonger sites when you want to move from Python basics into JavaScript or LLM application building.

JavaScript tutorialstutorial

LearnJSFast

Beginner-friendly JavaScript tutorials for practical web development and self-taught developers.

JavaScriptFrontendWeb development
Visit LearnJSFast
LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast

Keep learning

Related tutorials

Continue with nearby Python topics and beginner-friendly explanations.

A breathtaking sunrise over a vast mountainous landscape with clear skies.
beginner
6 min read

Defining Functions in Python

A function turns a block of code into a named tool you can call by name. Write the steps once, give them a name, and reuse them across your program instead…

Read tutorial