2025, Nov 17 13:00

Why type[Any] Doesn’t Mean the Any Class: Python Static Typing Pitfalls and Pyright/Pylance Notes

Learn why type[Any] widens to any class object, why Any is a special form in Python typing, and how Pyright/Pylance interpret unions, plus API design guidance.

When you try to accept a narrow set of classes plus the Any class itself in a function signature, it’s easy to slip into a trap where type checkers treat your annotation very differently from what you intended. The common instinct is to write a union of concrete class objects and type[Any], and perhaps even default the parameter to Any. That looks correct at a glance—but it changes the meaning of your API in ways that matter for static analysis.

Problem setup

Consider a function that should accept the class object int, the class object str, or the Any class itself, with Any as the default:

def run_op(kind: type[int] | type[str] | type[Any] = Any) -> Any:
    ...

This looks like a precise contract, but it isn’t. Using Any inside type[...] makes the annotation act like “any class object is allowed,” not “the class Any is allowed.” The fallout is twofold: you’ve effectively widened the parameter to every possible class, and a strict checker like Pylance can flag it with an error along the lines of Type "type[typing.Any]" is not assignable to declared type "builtins.type[Any]".

Why this happens

At type-checking time, Any isn’t treated as an ordinary class. It’s a special form that type checkers recognize and handle with bespoke logic. That special handling is the reason type[Any] means roughly “a class object of any type” rather than “the literal Any class object.” So the union in the example no longer enforces the intent.

At runtime, however, Any is implemented as a class, and typeshed also defines it as such. You can even inherit from it. That runtime behavior doesn’t change the fact that type checkers treat Any as a unique, opaque construct when interpreting type hints.

What fails if you try another path

You might try to lean on the runtime implementation detail by targeting the metaclass used under the hood. For example:

def run_alt(kind: type[int] | type[str] | _AnyMeta = Any) -> Any:
    ...

This approach breaks down in practice. You cannot rely on importing or using _AnyMeta from typing for annotations, and even if you could, it wouldn’t help: type checkers don’t treat Any as an instance of some class hierarchy—they treat it as a special form with no additional properties you can exploit via metaclass typing.

Runtime vs. type checking: a quick demonstration

The rift between runtime behavior and type-checker semantics shows up clearly if you subclass Any. At runtime, this is legal, and type checkers account for it by treating such subclasses as “effectively Any” for attribute access:

class X: ...
class Y(Any): ...

x = X()
y = Y()

x.token = 1   # strict checkers report an error: instances of X don't declare 'token'
y.token = 1   # accepted: instances of Y are treated like Any

Another revealing case is assigning Any to a variable annotated as type[Any]. Some type checkers accept it; Pyright/Pylance does not:

klass_ref: type[Any] = Any  # Pyright/Pylance: error
                              # Other checkers may accept it

Pyright’s maintainers explicitly document their stance:

[...] Any is not assignable to type. Any is a special form. Its implementation details are not documented or defined in typeshed. Type checkers should therefore not make assumptions about its runtime implementation.

What about TypeForm?

PEP 747 introduces TypeForm, which might seem like a way to express “the Any type itself.” However, the PEP does not define a way to specify the type of Any itself. Moreover, TypeForm[Any] doesn’t mean “just Any.” It describes a TypeForm whose type argument is not statically known but is a valid type form object and is therefore assignable both to and from any other TypeForm.

So what’s the practical resolution?

If you need a parameter that accepts specific class objects plus the Any class object itself, there is no checker-agnostic annotation that reliably expresses that intent today. Using type[Any] necessarily opens the door to every class object, which undermines the constraint. Targeting _AnyMeta doesn’t help, because Any is handled as a special form, not as an instance of a metaclass for the purposes of static typing.

Why this matters

The divergence between runtime behavior and type-checker semantics is easy to overlook, especially because Any can be subclassed at runtime and is defined as a class in typeshed. If you rely on runtime details when writing type hints, different checkers will disagree, and you can end up with annotations that either don’t enforce your contract or produce false errors in certain tools. Understanding that Any is special at type-checking time helps you avoid fragile or misleading annotations.

Conclusion

Don’t try to model the Any class itself as part of a union of class objects in a function’s parameter type. In static typing, Any is a special form, not a regular class, and type[Any] means “any class object,” not “the Any class.” Tools like Pyright/Pylance will reject constructs such as assigning Any to type[Any], and importing internal details like _AnyMeta won’t change how checkers interpret the annotation. If your API design hinges on distinguishing the Any class object from other class objects, recognize that current typing semantics don’t provide a portable, precise way to express that distinction.