W3docs

Sugerencias de tipo en Python

Aprende las sugerencias de tipo en Python: anotar variables, funciones y clases, usar el módulo typing y verificar tipos con mypy.

Las sugerencias de tipo permiten adjuntar información de tipo esperada a variables, parámetros de función y valores de retorno. Python no las aplica en tiempo de ejecución — son metadatos consumidos por editores, linters y comprobadores de tipos como mypy para detectar errores antes de ejecutar una sola línea.

Este capítulo cubre:

  • Por qué son importantes las sugerencias de tipo y cuándo usarlas
  • Anotar variables y funciones
  • Tipos integrados y el módulo typing (List, Dict, Optional, Union, Tuple, Any, Callable)
  • Sintaxis moderna (Python 3.10+)
  • Anotar clases y self
  • Genéricos y alias de tipo
  • Análisis estático con mypy
  • Errores comunes

¿Por qué usar sugerencias de tipo?

Python tiene tipado dinámico: una variable puede contener cualquier valor de cualquier tipo. Esa flexibilidad es poderosa, pero hace que los proyectos grandes sean más difíciles de navegar — no puedes saber el tipo del argumento de una función solo leyendo el lugar donde se llama.

Las sugerencias de tipo resuelven esto sin renunciar al dinamismo de Python:

  • Los editores detectan errores de inmediato. VS Code, PyCharm y otros subrayan los desajustes de tipo mientras escribes.
  • La refactorización se vuelve más segura. Cambia la firma de una función y el comprobador de tipos te indica cada llamada que se rompe.
  • El código se documenta a sí mismo. def greet(name: str) -> str comunica el contrato sin necesidad de una cadena de documentación.
  • Las bibliotecas son más fáciles de usar. Las bibliotecas tipadas exponen autocompletado para cada atributo y método.

Las sugerencias de tipo se introdujeron en Python 3.5 mediante PEP 484. La sintaxis se ha refinado en cada versión principal desde entonces. Los ejemplos a continuación indican la versión mínima de Python en que la sintaxis estuvo disponible por primera vez.

Anotar variables

Añade dos puntos después del nombre de la variable seguidos del tipo:

name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = True

También puedes declarar el tipo de una variable sin asignar un valor todavía. Esto se denomina declaración anticipada y es útil dentro de clases o a nivel de módulo:

user_id: int   # declared but not yet assigned
user_id = 42

Las anotaciones de tipo en variables a nivel de módulo no afectan el comportamiento en tiempo de ejecución — se almacenan en el diccionario __annotations__ del módulo, pero el intérprete las ignora por lo demás.

Anotar funciones

Coloca anotaciones en los parámetros (después de los dos puntos) y en el valor de retorno (después de -> antes de los dos puntos que cierran la firma):

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

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

def send_email(to: str, subject: str, body: str) -> None:
    print(f"Sending '{subject}' to {to}")

result: int = add(3, 5)
message: str = greet("Alice")

-> None significa que la función no tiene un valor de retorno significativo (devuelve None implícitamente). Omitir la anotación de retorno también es válido, pero -> None explícito deja clara la intención.

Parámetros con valor por defecto

Los valores por defecto van después de la anotación:

def connect(host: str, port: int = 8080, secure: bool = False) -> None:
    print(f"Connecting to {host}:{port} (secure={secure})")

connect("example.com")          # uses defaults
connect("example.com", 443, True)

*args y **kwargs

Anota el tipo del elemento, no el tipo de la colección:

def total(*prices: float) -> float:
    return sum(prices)

def create_user(**fields: str) -> dict:
    return fields

print(round(total(9.99, 4.50, 12.00), 2))   # 26.49
print(create_user(name="Bob", role="admin"))

*prices: float significa que cada argumento posicional es un float; en tiempo de ejecución prices sigue siendo una tuple ordinaria de floats. Del mismo modo, **fields: str significa que el valor de cada argumento nombrado es un str.

El módulo typing

Para cualquier cosa más allá de los tipos integrados básicos, importa desde el módulo typing (Python 3.5+). A partir de Python 3.9, muchos tipos de typing se fusionaron directamente en los equivalentes integrados (consulta Sintaxis moderna más adelante).

List, Tuple, Set, Dict

from typing import List, Tuple, Set, Dict

def first_names(users: List[str]) -> str:
    return users[0] if users else ""

def dimensions() -> Tuple[int, int, int]:
    return (1920, 1080, 32)

def unique_tags(items: List[str]) -> Set[str]:
    return set(items)

def word_count(text: str) -> Dict[str, int]:
    counts: Dict[str, int] = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

print(first_names(["Alice", "Bob"]))        # Alice
print(dimensions())                         # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"]))     # {'py', 'web'}
print(word_count("one two one"))            # {'one': 2, 'two': 1}

Optional

Optional[X] es una abreviatura de Union[X, None]. Úsalo cuando un valor puede estar ausente:

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    db = {1: "Alice", 2: "Bob"}
    return db.get(user_id)   # returns None if not found

name = find_user(1)
if name is not None:
    print(name.upper())      # ALICE

missing = find_user(99)
print(missing)               # None

Un comprobador de tipos ve Optional[str] y sabe que debes verificar si el valor es None antes de llamar a métodos de string en el resultado. Sin esa verificación, reporta un error.

Union

Union[X, Y] significa que el valor puede ser del tipo X o del tipo Y:

from typing import Union

def stringify(value: Union[int, float, str]) -> str:
    return str(value)

print(stringify(42))      # 42
print(stringify(3.14))    # 3.14
print(stringify("hi"))    # hi

Union es más útil cuando una función acepta genuinamente varios tipos no relacionados. Si escribes Union[str, None], usa Optional[str] en su lugar — es más idiomático.

Callable

Callable[[ArgTypes...], ReturnType] anota una función que se pasa como argumento:

from typing import Callable

def apply_twice(func: Callable[[int], int], value: int) -> int:
    return func(func(value))

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

print(apply_twice(double, 3))   # 12

Callable[[int], int] significa: un callable que toma un argumento int y devuelve un int. Si la lista de argumentos es compleja o desconocida, usa Callable[..., ReturnType].

Any

Any es un tipo especial que desactiva la comprobación de tipos para ese valor. Cada tipo es asignable a Any y también asignable desde Any:

from typing import Any

def log(value: Any) -> None:
    print(value)

log(42)
log("hello")
log([1, 2, 3])

Usa Any con moderación — es una escotilla de escape que elimina la protección que ofrecen las sugerencias de tipo. Es apropiado al interactuar con código de terceros sin tipos, o durante una migración gradual de un proyecto grande.

Sintaxis moderna (Python 3.9+, 3.10+)

Genéricos integrados (Python 3.9+)

A partir de Python 3.9, puedes usar los tipos integrados directamente como genéricos, sin importar desde typing:

# Python 3.9+
def word_count(text: str) -> dict[str, int]:
    counts: dict[str, int] = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

def first(items: list[int]) -> int | None:
    return items[0] if items else None

print(word_count("cat dog cat"))    # {'cat': 2, 'dog': 1}
print(first([10, 20, 30]))         # 10
print(first([]))                   # None

Usa list[str] en lugar de List[str], dict[str, int] en lugar de Dict[str, int], y así sucesivamente.

Sintaxis de unión X | Y (Python 3.10+)

Python 3.10 introdujo el operador | para uniones, reemplazando Union[X, Y] y Optional[X]:

# Python 3.10+
def parse(value: str | int | None) -> str:
    if value is None:
        return "nothing"
    return str(value)

print(parse("hello"))   # hello
print(parse(42))        # 42
print(parse(None))      # nothing

str | None es equivalente a Optional[str]. Esta sintaxis es más limpia y fácil de leer.

Anotar clases

Anota los atributos de instancia dentro de __init__ y agrega anotaciones de retorno a los métodos:

class BankAccount:
    owner: str        # class-level annotation (no default value)
    balance: float

    def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
        self.owner = owner
        self.balance = initial_balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount

    def withdraw(self, amount: float) -> bool:
        if amount > self.balance:
            return False
        self.balance -= amount
        return True

    def __repr__(self) -> str:
        return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"

account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0))   # True
print(account)                  # BankAccount(owner='Alice', balance=120.00)

La anotación de self siempre se infiere — nunca escribes self: BankAccount. El tipo de retorno de __init__ es siempre None.

ClassVar

Usa ClassVar[T] (de typing) para marcar un atributo que pertenece a la clase y no a cada instancia:

from typing import ClassVar

class Config:
    MAX_RETRIES: ClassVar[int] = 3
    timeout: int

    def __init__(self, timeout: int) -> None:
        self.timeout = timeout

print(Config.MAX_RETRIES)   # 3

Un comprobador de tipos advierte si intentas establecer ClassVar en una instancia — está destinado a compartirse a nivel de clase.

Alias de tipo

Un alias de tipo da a un tipo largo o complejo un nombre más corto y significativo:

from typing import List, Tuple

# Simple alias
UserID = int
Filename = str

# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]

def distance(p1: Coordinates, p2: Coordinates) -> float:
    return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5

print(distance((0.0, 0.0), (3.0, 4.0)))   # 5.0

A partir de Python 3.12, usa la instrucción type para crear alias explícitos e inspeccionables:

# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]

Genéricos con TypeVar

TypeVar te permite escribir una sola función que funcione con cualquier tipo mientras preserva las relaciones de tipo:

from typing import TypeVar, List

T = TypeVar("T")

def first_item(items: List[T]) -> T:
    return items[0]

x: int = first_item([1, 2, 3])       # x is int
s: str = first_item(["a", "b"])      # s is str

El comprobador de tipos infiere a partir del argumento cuál es T y lleva esa información al tipo de retorno. Sin TypeVar, tendrías que devolver Any y perder la seguridad de tipos.

Puedes restringir TypeVar a un conjunto de tipos permitidos:

from typing import TypeVar

Numeric = TypeVar("Numeric", int, float)

def double(n: Numeric) -> Numeric:
    return n * 2

print(double(4))      # 8   (int)
print(double(2.5))    # 5.0 (float)

Comprobación de tipos estática con mypy

mypy es el comprobador de tipos estático más utilizado para Python. Instálalo con pip:

pip install mypy

Luego ejecútalo sobre un archivo:

mypy my_script.py

Ejemplo: detectar un error con mypy

Guarda lo siguiente como demo.py:

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

result = greet(42)   # passing int instead of str
print(result.upper())

Al ejecutar mypy demo.py se reporta:

demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

Python en sí ejecuta el código sin problemas (los f-strings convierten cualquier tipo), pero mypy detectó el desajuste antes de que tuvieras que descubrirlo en producción.

Opciones útiles de mypy

OpciónEfecto
--strictActiva todas las comprobaciones opcionales (recomendado para proyectos nuevos)
--ignore-missing-importsSuprime errores sobre stubs de terceros faltantes
--check-untyped-defsTambién comprueba tipos en funciones sin anotaciones
--disallow-untyped-defsExige anotaciones en todas las definiciones de función

Un archivo mypy.ini (o [tool.mypy] en pyproject.toml) mantiene la configuración fuera de la línea de comandos:

[mypy]
strict = true
ignore_missing_imports = true

Tipado gradual

No tienes que anotar todas las funciones de una vez. Python admite el tipado gradual: el código anotado y el no anotado coexisten pacíficamente. mypy omite las funciones sin anotar por defecto (a menos que se establezca --check-untyped-defs).

Un enfoque práctico para un proyecto existente:

  1. Añade anotaciones al código nuevo desde el primer día.
  2. Anota primero las funciones más llamadas o más propensas a errores.
  3. Activa --strict módulo a módulo a medida que mejora la cobertura.
  4. Usa Any solo donde una biblioteca de terceros no tenga tipos, y agrega un comentario explicando por qué.

Errores comunes

Referencias anticipadas

Si un tipo hace referencia a una clase definida más adelante en el mismo archivo, pon el nombre entre comillas para convertirlo en una cadena (una referencia anticipada):

class Node:
    def __init__(self, value: int, next: "Node | None" = None) -> None:
        self.value = value
        self.next = next

head = Node(1, Node(2))
print(head.value, head.next.value)   # 1 2

A partir de Python 3.10+, agrega from __future__ import annotations al inicio del archivo. Esto convierte todas las anotaciones en cadenas lazy y elimina la necesidad de poner comillas manualmente.

Anotaciones en tiempo de ejecución

Por defecto, las anotaciones en Python 3.9 y anteriores se evalúan de forma ansiosa. Eso significa que una referencia anticipada sin comillas lanza un NameError:

# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...

Con from __future__ import annotations (Python 3.7+), todas las anotaciones se almacenan como cadenas y solo se evalúan cuando se inspeccionan — lo que resuelve el problema de las referencias anticipadas automáticamente.

None vs Optional

Un error frecuente es anotar un tipo de retorno como str cuando la función puede devolver None. Usa siempre Optional[str] (o str | None) cuando None sea un posible retorno:

from typing import Optional

# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
    if user_id == 0:
        return None   # type: ignore  — this is the bug

# Correct
def get_name_safe(user_id: int) -> Optional[str]:
    if user_id == 0:
        return None
    return "Alice"

list vs List (compatibilidad de versiones)

Si tu código se ejecuta en Python 3.8 o anterior, debes usar from typing import List y escribir List[str]. En Python 3.9+, list[str] funciona directamente. Si necesitas compatibilidad con ambos, usa las importaciones de typing o añade from __future__ import annotations.

Referencia rápida

AnotaciónSignificado
x: intLa variable x es un entero
def f(a: str) -> boolEl parámetro a es str; el valor de retorno es bool
-> NoneLa función no devuelve nada significativo
Optional[str]str o None
Union[int, str]int o str
list[int] / List[int]Lista de enteros
dict[str, int] / Dict[str, int]Dict que mapea str a int
tuple[int, str] / Tuple[int, str]Tupla de (int, str)
Callable[[int], str]Función que toma int y devuelve str
AnyCualquier tipo (desactiva la comprobación)
ClassVar[T]Atributo a nivel de clase
TypeVar("T")Variable de tipo genérica

Temas relacionados

Práctica

Práctica
What does Optional[str] mean in a Python type hint?
What does Optional[str] mean in a Python type hint?
Práctica
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Práctica
What is the purpose of TypeVar in the typing module?
What is the purpose of TypeVar in the typing module?
Was this page helpful?