2025, Dec 23 13:00

How to Type a Generic Factory in Python: Avoid Pyright errors with Type[R], type[T], and Protocol

Learn why Pyright flags 'Expected 0 positional arguments' with Type[R] and how to fix it using type[T] or a Protocol for a str-only constructor in factory code.

When a generic factory-like function calls a class constructor with a string, Pyright can complain even though the code runs at runtime. The root cause isn’t in the call site, but in how the type of the constructor is declared. Let’s unpack the issue, see why the type checker is right to be cautious, and apply two practical ways to type this pattern.

Problem example

Consider a minimal slice that exhibits the error. The intent is to accept a type and return an instance created from a string.

from typing import Type, Optional

def build_item[R](text: str, cls: Type[R]) -> Optional[R]:
    return cls(text)

Pyright reports:

[reportCallIssue]: Expected 0 positional arguments

What’s actually wrong

An unbounded Type parameter like Type[R] is effectively treated as Type[object]. The initializer of object does not accept positional arguments, so calling cls(text) conflicts with that assumption. From Pyright’s perspective, you are passing one positional argument to something that is known only to be object, hence the “Expected 0 positional arguments” diagnostic.

Two ways to type it safely

One approach is to be less specific and only assert that a type was passed, without constraining its constructor signature. Bind the type variable to Any, and annotate the constructor parameter using the builtin type. This tells the checker to validate that the second argument is indeed a type, while not checking the signature of its __init__.

from typing import Any, Optional

# Bind R to Any

def build_item_any[R: Any](text: str, ctor: type[R]) -> Optional[R]:
    return ctor(text)  # OK

build_item_any("foo", "not_a_class")  # Error
build_item_any("foo", str)  # OK
build_item_any("foo", dict)  # OK, but bad

If you need stronger guarantees about the init signature, define a Protocol that describes “constructible from a single positional str”. One notable restriction is to model a single positional-only parameter using the slash; without it, keyword-only or extra parameters would slip through. There is a caveat: some builtins like int or str have overloaded initializers that aren’t covered by this narrow protocol. In such cases you will see errors, and you may either suppress them or adjust the Protocol as needed. For older Python versions, Pyright also treats double-underscored names as positional parameters.

from typing import Optional, Protocol, Type

class AcceptsSingleStr(Protocol):
    # Positional-only init
    def __init__(self, value: str, /): ...


def build_item_checked[S: AcceptsSingleStr](text: str, factory: Type[S]) -> Optional[S]:
    return factory(text)


class Foo:
    def __init__(self, s: str): ...

# More than one argument should error
class NeedsTwoArgs:
    def __init__(self, s: str, extra): ...

class KwOnlyCtor:
    def __init__(self, *, keyword: str): ...

# Extra args but with a default
class WithDefaultArg:
    def __init__(self, s: str, ok=None): ...

build_item_checked("foo", Foo)  # OK
build_item_checked("foo", WithDefaultArg)  # OK
build_item_checked("foo", "not_a_class")  # Error, as expected
build_item_checked("foo", NeedsTwoArgs)  # Error, as expected
build_item_checked("foo", dict)  # Error, as expected
# Overloaded classes for parameterless initialization need to be ignored
build_item_checked("foo", str)   # Error, but should be okay
build_item_checked("1", int)     # Error, but should be okay

Why this matters

This pitfall is a good reminder that Type without a bound defaults to Type[object], and object’s zero-arg initializer cascades into false positives for constructor calls with arguments. It also raises two practical design questions that are easy to overlook in a reduced example. First, a type like type(None) won’t accept a string, and in general no type should produce None instead of an instance of itself; that challenges whether Optional is truly intended. Second, decide whether you care that the second parameter is specifically a type, or whether you only require “something callable on a string that yields a value.” That choice influences whether you model the parameter as a type or as a callable-like contract.

Takeaways

If you just need to accept any type and defer constructor validation, bind the generic to Any and use type[T] for the parameter. If you want compile-time safety for a specific constructor shape, introduce a Protocol with a positional-only str parameter and bind the generic to it, acknowledging that overloaded builtins like int and str won’t fit without adjustments. Being explicit about the intended contract prevents noisy diagnostics and makes your API intent unmistakable to both the type checker and future readers.