Python Typing for New Developers
A beginner‑friendly guide to adding type hints in Python.
Note: This tutorial assumes you understand some basic Python syntax, but are new to programming or type hints. To learn more about Python, see the Python Tutorial and Getting Started Guide
1. What is a Type?
A type is a classification that defines what operations can be performed on a piece of data, what values it can hold, and how it behaves in memory. Types are fundamental to programming because they help ensure that operations on data make sense.
For example:
- An
int
(integer) type can be added, subtracted, or multiplied - A
str
(string) type can be concatenated or split - A
list
type can be indexed, sliced, or iterated over
Note: These are just examples of common operations for each data type. Python's built-in types support many more operations that are not listed here.
Understanding types helps you predict how your code will behave and avoid runtime errors from trying to perform operations that don't make sense, such as dividing a string by a number.
2. What is a Type Hint in Python?
A type hint in Python is a way to indicate the expected data type of a variable, function parameter, or return value. It's a hint to other developers (and to tools like type checkers and IDEs) about what type of data should be used with a particular piece of code.
Type hints are not enforced at runtime by Python itself, but they can be used by third-party tools (like Pyrefly) to catch type-related errors before your code runs. They also serve as documentation, making it easier for others to understand how to use your code. Here's an example of a simple function with type hints:
def greet(name: str) -> None:
print(f"Hello, {name}!")
In this example:
name: str
indicates that thename
parameter should be a string.-> None
specifies that the function doesn't return any value (similar tovoid
in other languages).
3. Why Bother with Type Hints?
Python is a dynamically typed language, which means you can write code without declaring types. However, this can lead to bugs or ambiguity in your code.
TL;DR
- Catch bugs before running the code.
- Improve editor autocomplete & refactors.
- Turn your code into living documentation.
In this example:
- The first function lacks type hints, making it unclear what types
text
andtimes
should be. The*
operator works differently depending on types (string repetition, list repetition, or multiplication). - The second function uses type hints to clearly indicate that
text
should be a string,times
should be an integer, and the function returns a string. - This clarity helps prevent bugs like accidentally passing a string for
times
or using the function incorrectly.
Can you spot the bug?
class Rectangle:
width: int
height: int
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
rect = Rectangle(width=100, height=50)
area = rect.width * rect.hieght
print(area)
In this example:
- The bug is a typo in
rect.hieght
(should berect.height
). - Without type hints, Python would only report this error at runtime when it tries to access the non-existent attribute.
- With type hints and a tool like Pyrefly, this error would be caught before running the code because the
Rectangle
class has defined attributeswidth
andheight
, but nothieght
.
Spelling is hard! Let's add the dataclass
decorator to our class definition. This will generate a constructor for us, and also add a few other useful methods.
In this dataclass example:
- The
@dataclass
decorator automatically generates methods like__init__
,__repr__
, and__eq__
based on the class attributes. - Type hints are used to define the class attributes (
width: int
,height: int
). - The same spelling error exists (
rect.hieght
), but tools like Pyrefly can catch this before runtime because the dataclass clearly defines which attributes exist. - This demonstrates how type hints combined with dataclasses provide both convenience and better error detection.
4. Primitive Types
Since Python 3.9 you can use all the primitive types directly as annotations.
In this primitive types example:
- Each variable is annotated with its expected type (
int
,float
,str
,bool
). - The values assigned match their declared types.
- These annotations help document the code and allow type checkers to verify that operations on these variables are valid for their types.
- For example, a type checker would flag an error if you tried
age + name
since adding an integer and string isn't a valid operation.
You can also specify a parameter as optional by using Optional
type, or now with the | None
syntax.
In this Optional type example:
- Both variables can either be a string or
None
. Optional[str]
is the traditional syntax (pre-Python 3.10).str | None
is the newer union syntax introduced in Python 3.10.- These annotations tell type checkers that the variable might be
None
, so they can warn you if you try to perform string operations without checking forNone
first.
5. Collections
Syntax Examples
- List of numbers
list[int] scores: list[int] = [98, 87, 91]
- Tuple of two floats
tuple[float, float] point: tuple[float, float] = (3.0, 4.0)
- Dict of str -> int
dict[str, int] inventory: dict[str, int] = {"apples": 5}
- Set of strings
set[str] authors: set[str] = {"Bob", "Eve"}
Since Python 3.9 you can subscript built‑ins directly—no need for from typing import List
.
6. Functions
In this basic function example:
- Both parameters
a
andb
are annotated as integers. - The function is annotated to return an integer (
-> int
). - This tells type checkers that the function should only be called with integers and that the return value should only be used in contexts where an integer is expected.
Default values keep their annotation:
In this function with default values:
- The
name
parameter must be a string. - The
polite
parameter is a boolean with a default value ofTrue
. - The function returns a string.
- Even though
polite
has a default value, it still has a type annotation to ensure that if it's explicitly provided, it must be a boolean.
Variable‑length arguments:
In this variable-length arguments example:
Logger
is defined as a type alias for a callable that takes a string and returns nothing (None
).*msgs: str
indicates that the function accepts any number of string arguments.log: Logger | None
means thelog
parameter can be either a Logger function orNone
.- The function is annotated to return
None
. - This demonstrates how to type complex function signatures with variable arguments and function parameters.
7. Get Type Hint Signals Directly in Your Editor
You can download the Pyrefly extension for VSCode to get type hint signals directly in your IDE.
Next, install Pyrefly and check some code:
# Fast, zero‑config
pip install pyrefly
pyrefly check ./my_sample.py
# Check whole directories
pyrefly check app/ tests/
Create a pyrefly.toml
file to configure your project. Instructions here.