ClassVar en Python: separa clase e instancia

Publicado el: 29/08/2026
Tempo de leitura: 6 minutos
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

En Python, un atributo definido en el cuerpo de una clase puede representar configuración compartida, un registro, un contador, una constante o simplemente un valor por defecto que cada instancia reemplaza. El runtime permite todos esos patrones, pero la ambigüedad puede confundir a lectores, dataclasses y analizadores. typing.ClassVar declara explícitamente que un atributo pertenece a la clase y no debe tratarse como campo de instancia.

Esta guía explica ClassVar en clases normales y dataclasses, herencia, sombreado, caches, registries, contadores, interacción con Final y riesgos del estado mutable compartido.

El problema de la ambigüedad

class Usuario:
    total = 0
    nombre = ""

total parece un contador compartido y nombre parece información por instancia. Sin inicialización y anotaciones claras, las herramientas no distinguen bien la intención.

Declarar ClassVar

from typing import ClassVar

class Usuario:
    total: ClassVar[int] = 0

    def __init__(self, nombre: str) -> None:
        self.nombre = nombre
        type(self).total += 1

ClassVar indica que total pertenece a la clase. El analizador puede advertir cuando se usa como si fuera un campo individual.

Acceso por clase e instancia

print(Usuario.total)
usuario = Usuario("Ana")
print(usuario.total)

Las reglas de lookup permiten leer un atributo de clase desde una instancia. Aun así, prefiere Usuario.total o type(usuario).total cuando quieres expresar estado compartido.

Asignar por instancia crea sombreado

usuario.total = 100

En una clase normal, esta asignación puede crear un atributo en la instancia que oculta el valor de clase para ese objeto. El contador compartido sigue en Usuario.total. ClassVar ayuda al checker, pero no cambia el comportamiento de runtime.

ClassVar en dataclasses

from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Producto:
    impuesto_predeterminado: ClassVar[float] = 0.1
    nombre: str
    precio: float

La dataclass no trata impuesto_predeterminado como campo. No aparece en el __init__ generado, no participa como campo en comparaciones y no aparece en la enumeración de fields.

producto = Producto("Teclado", 200.0)

Sin ClassVar, el atributo podría convertirse en campo de instancia y modificar el constructor.

Valores mutables compartidos en dataclasses

@dataclass
class Catalogo:
    cache: ClassVar[dict[str, object]] = {}
    nombre: str = "principal"

El diccionario es compartido por todas las instancias. Puede ser intencional, pero necesita política de limpieza, decisiones de concurrencia y pruebas aisladas. ClassVar no vuelve inmutable ni thread-safe al objeto.

Estado mutable compartido

Listas y diccionarios de clase viven más que las instancias y pueden persistir entre peticiones o pruebas.

class Registro:
    items: ClassVar[dict[str, type]] = {}

    @classmethod
    def registrar(cls, nombre: str, tipo: type) -> None:
        cls.items[nombre] = tipo

Un registry de plugins es un uso razonable. Datos específicos de una petición no lo son. Encapsula mutaciones, ofrece reset y considera sincronización.

ClassVar con classmethod

class Contador:
    valor: ClassVar[int] = 0

    @classmethod
    def incrementar(cls) -> int:
        cls.valor += 1
        return cls.valor

El classmethod recibe la clase concreta. Las subclases pueden compartir o crear su propio valor según dónde se realice la asignación.

Herencia y configuración por subclase

class Base:
    limite: ClassVar[int] = 10

class Premium(Base):
    limite = 100

Premium define su propio atributo y otras subclases heredan Base.limite. Es útil para configuración polimórfica. Si la redefinición debe prohibirse, considera Final.

Actualizar mediante cls

class Base:
    llamadas: ClassVar[int] = 0

    @classmethod
    def registrar_llamada(cls) -> None:
        cls.llamadas += 1

Al llamarlo desde una subclase, la operación puede crear un atributo propio en esa subclase. Si el contador debe ser global para toda la jerarquía, actualiza Base.llamadas explícitamente o mueve el estado a otro objeto.

ClassVar y Final

ClassVar significa que el atributo pertenece a la clase. Final significa que no debería redefinirse. Las intenciones pueden coincidir, pero las combinaciones exactas dependen de la versión y del analizador.

from typing import Final

class Protocolo:
    VERSION: Final[str] = "1"

Para un valor fijo en toda la jerarquía, Final en el cuerpo suele ser suficiente. Para estado compartido configurable, usa ClassVar.

ClassVar y propiedades

Una propiedad representa acceso calculado de instancia; ClassVar describe almacenamiento o configuración de clase.

class Circulo:
    pi: ClassVar[float] = 3.141592653589793

    def __init__(self, radio: float) -> None:
        self.radio = radio

    @property
    def area(self) -> float:
        return self.pi * self.radio ** 2

ClassVar en Protocol

Un Protocol puede describir un atributo de clase, aunque el soporte detallado varía entre analizadores.

from typing import Protocol

class Serializavel(Protocol):
    formato: ClassVar[str]

    def serializar(self) -> bytes: ...

El contrato indica que las implementaciones exponen una configuración de clase llamada formato.

Factories y registries

class Conversor:
    _formatos: ClassVar[dict[str, type["Conversor"]]] = {}

    def __init_subclass__(cls, *, formato: str, **kwargs) -> None:
        super().__init_subclass__(**kwargs)
        Conversor._formatos[formato] = cls

    @classmethod
    def crear(cls, formato: str) -> "Conversor":
        tipo = cls._formatos[formato]
        return tipo()

El registry pertenece a la familia de clases, no a cada conversor. Usa una referencia explícita a la base cuando el mapa debe ser único.

Caches de clase

class Parser:
    _cache: ClassVar[dict[str, object]] = {}

    @classmethod
    def compilar(cls, expresion: str) -> object:
        if expresion not in cls._cache:
            cls._cache[expresion] = compilar_expresion(expresion)
        return cls._cache[expresion]

Considera límites de memoria, invalidación, concurrencia y si las subclases deben compartir cache. functools.lru_cache o un servicio externo pueden ser opciones mejores.

Contadores de instancias

class Conexion:
    abiertas: ClassVar[int] = 0

    def __init__(self) -> None:
        type(self).abiertas += 1

    def cerrar(self) -> None:
        type(self).abiertas -= 1

La lógica puede fallar con cierres dobles, excepciones, threads o subclases. ClassVar solo describe dónde vive el estado.

ClassVar y slots

__slots__ controla almacenamiento de instancia. Los atributos de clase siguen en el objeto clase. ClassVar documenta la separación, pero no reemplaza slots ni cambia memoria.

ClassVar sin parámetro

configuracion: ClassVar = {}

Puede aceptarse, pero un tipo interno explícito mejora el análisis:

configuracion: ClassVar[dict[str, str]] = {}

No usar ClassVar para defaults de instancia

@dataclass
class Tarea:
    prioridad: ClassVar[int] = 1
    titulo: str = ""

Si cada tarea necesita prioridad propia, elimina ClassVar:

@dataclass
class Tarea:
    titulo: str
    prioridad: int = 1

Serialización

Los serializadores de dataclasses suelen ignorar ClassVar porque no es field. Las herramientas basadas en vars(instancia) tampoco ven atributos que solo existen en la clase. Incluye configuración compartida explícitamente si debe salir en JSON.

Aislamiento de pruebas

El estado de clase puede filtrarse entre tests. Limpia registries y caches en fixtures o ofrece métodos dedicados:

@classmethod
def limpiar_cache(cls) -> None:
    cls._cache.clear()

No dependas del orden de ejecución de pruebas.

Concurrencia

ClassVar no vuelve atómicas las operaciones. Contadores, registries y caches compartidos pueden requerir locks, colas, almacenamiento seguro para procesos u otra estrategia.

Cuándo el estado de módulo es más claro

Si el estado no pertenece conceptualmente a la clase, una variable privada de módulo puede ser más simple. Usa ClassVar cuando la configuración o registry forme parte del contrato y pueda especializarse por herencia.

Cuándo preferir composición

Registries, caches y contadores complejos pueden ser objetos independientes inyectados en consumidores. La composición mejora aislamiento y pruebas. ClassVar es cómodo, pero no debe convertirse en almacenamiento global invisible.

Errores comunes

  • Usar ClassVar para un campo de dataclass: desaparece del constructor.
  • Asignar por instancia: puede crear sombreado.
  • Compartir una colección mutable sin intención: las instancias interfieren.
  • Ignorar herencia: las subclases pueden compartir o separar estado.
  • Esperar seguridad de concurrencia: solo es una anotación.
  • Usarlo como global disfrazado: las dependencias son difíciles de probar.

Ejemplo completo: codecs registrados

from typing import ClassVar

class Codec:
    _tipos: ClassVar[dict[str, type["Codec"]]] = {}
    nombre: ClassVar[str]

    def __init_subclass__(cls, **kwargs) -> None:
        super().__init_subclass__(**kwargs)
        nombre = getattr(cls, "nombre", None)
        if nombre:
            Codec._tipos[nombre] = cls

    @classmethod
    def crear(cls, nombre: str) -> "Codec":
        try:
            tipo = Codec._tipos[nombre]
        except KeyError as error:
            raise ValueError(f"codec desconocido: {nombre}") from error
        return tipo()

    def codificar(self, texto: str) -> bytes:
        raise NotImplementedError

class Utf8Codec(Codec):
    nombre: ClassVar[str] = "utf-8"

    def codificar(self, texto: str) -> bytes:
        return texto.encode("utf-8")

El mapa es único para la jerarquía y cada subclase publica un nombre de clase. Ninguno es campo de instancia.

Buenas prácticas

Anota el estado compartido. Accede mediante la clase. Encapsula mutaciones en classmethods. Documenta si las subclases comparten o reemplazan valores. Evita colecciones mutables públicas. Ofrece limpieza para tests y trata la concurrencia conscientemente.

Conclusión

typing.ClassVar separa atributos de clase de campos de instancia y es especialmente importante en dataclasses, registries, caches, contadores y configuración de jerarquías. Mejora la claridad estática, pero no cambia las reglas de lookup o mutación de Python.

La documentación oficial de ClassVar en Python define su uso. Combínalo con encapsulación, políticas de herencia, aislamiento de pruebas y sincronización cuando el estado compartido sea mutable.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    A person reads 'Python for Unix and Linux System Administration' indoors.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    overload en Python: firmas precisas

    Aprende typing.overload en Python para firmas precisas con Literal, None, genéricos, métodos y retornos dependientes de argumentos.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Macro shot capturing detailed patterns of a python in its natural surroundings.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Final en Python: protege constantes y herencia

    Aprende Final y @final en Python para proteger constantes, atributos, métodos y clases, comprendiendo los límites en runtime.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Annotated en Python: tipos con metadatos

    Aprende Annotated en Python para añadir metadatos a tipos, crear validación, schemas, unidades e integraciones con frameworks.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    NewType en Python: separa identificadores

    Aprende NewType en Python para separar IDs, códigos y valores primitivos, validar fronteras y evitar mezclas de dominio.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Vivid close-up of code on a computer screen showcasing programming details.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Never en Python: marca código inalcanzable

    Aprende typing.Never en Python para funciones sin retorno, código inalcanzable y exhaustividad con assert_never, Literal y Enum.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate en Python: cambia parámetros

    Aprende Concatenate en Python para añadir u ocultar parámetros iniciales en decoradores tipados con ParamSpec y dependencias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026