Python Set Type Hints: Syntax and Usage
python set type hint: Learn how to annotate sets in Python with type hints, including set[int], typing.Set, function signatures, and compatibility across Python versions.
When you annotate a variable as a set in Python, you have more than one syntax to choose from. The correct choice depends on your Python version and whether you are using the typing module or the built-in generic syntax introduced in Python 3.9. This article covers the practical ways to write python set type hint annotations, how they behave in function signatures, and what to watch for when your code must run on older interpreters.
Declaring a Set Type Hint
The most direct way to hint that a variable holds a set of integers is:
scores: set[int] = {90, 85, 77}
This syntax works in Python 3.9 and later because the built-in set became subscriptable with PEP 585. Before that, you needed to import Set from typing:
from typing import Set scores: Set[int] = {90, 85, 77}
Both forms mean the same thing to static type checkers such as mypy and Pyright. The runtime does not enforce either annotation. If you assign a list to scores after that declaration, Python will not raise an error; only a static analysis tool will flag the mismatch.
The typing.Set form remains valid in all Python versions, but the built-in generic syntax is shorter and reads more naturally. If you are targeting Python 3.9 or newer, prefer set[int]. For code that must support Python 3.8 or earlier, use typing.Set or add from __future__ import annotations to defer evaluation of annotations.
Using Set Type Hints in Function Signatures
Set type hints are most useful in function signatures because they document the expected input and output shape. Consider a function that removes duplicates from a list by converting it to a set:
def unique(values: list[int]) -> set[int]: return set(values)
The return annotation set[int] tells callers that the result is a set of integers. If the function can return an empty set, that is still valid because set[int] does not imply non-emptiness.
When the set contains more complex objects, use the appropriate type parameter. For example, a set of user IDs that are strings:
def active_user_ids(users: list[User]) -> set[str]: return {user.id for user in users if user.active}
Here set[str] communicates that the function returns a set of string identifiers. Static checkers can then verify that code consuming the result treats it as a set of strings, not as a set of User objects.
Type Aliases for Reusable Set Types
If a particular set type appears in many places, define a type alias to avoid repeating the full annotation and to make the intent clearer.
UserID = int UserIDSet = set[UserID] def get_friends(user_id: UserID) -> UserIDSet: ...
The alias UserIDSet can be used anywhere a set of integers is expected. This reduces the chance of writing set[str] by mistake and makes future changes easier. If the underlying type of user IDs changes from int to str, you can update the alias in one place.
Type aliases also work with typing.Set:
from typing import Set UserIDSet = Set[int]
The same principle applies, but the built-in syntax is preferred when possible.
Generic Functions with Set Types
When you need a function that works with sets of any type, use TypeVar to keep the type relationship intact.
from typing import TypeVar T = TypeVar("T") def union_sets(left: set[T], right: set[T]) -> set[T]: return left | right
The T type variable ensures that both arguments and the return value share the same element type. If you call union_sets({1, 2}, {"a"}), a static checker will report an error because the element types do not match. Without the type variable, you would have to use set[Any], which loses type safety.
Generic functions are especially valuable in library code where the element type is unknown in advance. They let the caller retain precise type information without sacrificing flexibility.
Class Attributes and Instance Variables
Set type hints also apply to class attributes and instance variables. Annotating them helps document the expected structure of an object.
class Inventory: item_ids: set[int] def __init__(self) -> None: self.item_ids = set()
Here item_ids is declared as a set of integers. In an instance method, you can assign a new set to it:
def add_item(self, item_id: int) -> None: self.item_ids.add(item_id)
Static checkers will flag assignments that do not match the declared type, such as self.item_ids = [1, 2]. This is particularly useful in large codebases where the shape of an object may not be obvious from its constructor alone.
Compatibility Across Python Versions
The main compatibility issue with set type hints is the availability of the built-in generic syntax. Python 3.9 introduced PEP 585, which made built-in collections like set, list, and dict subscriptable. If you are using Python 3.8 or earlier, you have two options:
- Use
typing.Setinstead ofset[...]. - Add
from __future__ import annotationsat the top of your module.
The from __future__ import changes how annotations are evaluated. Instead of being evaluated at definition time, they are stored as strings and evaluated later. This allows you to write set[int] even on Python 3.8, because the annotation is not executed until a type checker or a tool like typing.get_type_hints() resolves it.
However, from __future__ import annotations affects all annotations in the module, not just sets. It can change runtime behavior if you rely on annotations for reflection. Most applications do not, but be aware of the tradeoff.
Runtime Behavior and Static Analysis
Set type hints have no effect on runtime performance. They are not enforced by the interpreter, and they do not change how the set is stored or accessed. The annotation is stored in the __annotations__ attribute of the function or class, but it is not used for any runtime optimization or validation.
This means that a type hint will not prevent you from accidentally passing a list where a set is expected. It will not raise a TypeError if you try to add a string to a set that was annotated as set[int]. The value of the annotation is entirely in static analysis: tools like mypy, Pyright, and Pylance can catch mismatches before the code runs.
For performance-sensitive code, the absence of runtime overhead is a benefit. You get documentation and compile-time checking without paying a cost at runtime. The only cost is the negligible memory used to store the annotation itself, which is typically not a concern.