32

mypy complains error: Variable "packagename.Foo" is not valid as a type

Foo = type('Foo', (), {})
Bar = Optional[Foo]

This error can be fixed by defining the type as a class:

class Foo:
    pass

Bar = Optional[Foo]

Is there any other way around this? I need to keep the type definition dynamic.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
user3534080
  • 1,201
  • 1
  • 14
  • 21
  • 2
    why do you want to use `type()` like that? note that `mypy` doesn't run/evaluate code so I don't think `type` would be executed to "make a new type object" – Sam Mason Oct 28 '19 at 16:29
  • 9
    I guess it comes for the fact that mypy [can't handle dynamic base classes](https://github.com/python/mypy/issues/2477). – frankie567 Oct 28 '19 at 16:45
  • 1
    Note that the above link provides a workaround: `Foo = type(...) # type: Any`. (I'm not sure which specific type is required; `type: type` does not work.) – chepner Oct 28 '19 at 16:50
  • 5
    Generally, a static type checker is not going to be able to handle dynamically created types. – juanpa.arrivillaga Oct 28 '19 at 18:04
  • 5
    What I don't really understand is that if you "promise" the type checker that your variable will be of the class type `a: Type[Foo] = Foo`, why doesn't it consider valid `class Bar(a)`? We can do this in TypeScript without any problem: http://www.typescriptlang.org/play/#code/MYGwhgzhAECC0G9oF8BQqBmBXAdsALgJYD2O0okEAYmAcQE4CeAFKBAFzT6MAOApsQxwAlIlTRy4KNABC0PgA98fHABMYbMeJToJ9Pviz0yMgNyo0qYKQj5Z0ALyTKNOk2axh5oA – frankie567 Oct 30 '19 at 08:11
  • As @chepner suggests, `# type: Any` is the only sane answer. – Cecil Curry Feb 18 '21 at 05:55
  • 3
    I was going to suggest that the difference is that `class`, as a syntactic construct, *must* produce a type, whereas `type` can be rebound to any callable at runtime. However, the `class` statement can also be modified by specifying a different metaclass. In the end, keep in mind that `mypy` is really a hack (in the best sense of the word) trying to squeeze as much static typing as possible out of the inherently dynamic data model that Python is based on. There are many corner cases it has to work around. – chepner Feb 18 '21 at 12:41
  • another sane answer might be to refactor so that you can use an Enum – Goodword Jul 29 '21 at 18:13

3 Answers3

6

How about this, as a workaround?

from typing import Optional, TYPE_CHECKING

if TYPE_CHECKING:
    class Foo: pass
else:
    Foo = type('Foo', (), {})
    
Bar = Optional[Foo]

typing.TYPE_CHECKING is a constant that will always be True at compile-time, and will always be False at runtime. In this way, we can keep MyPy happy by only telling it about the static definition, but at runtime we can be as dynamic as we like.

You should be aware, though, that this is very much a workaround rather than a solution. By doing it this way, we are essentially lying to the type-checker about the true definition of Foo. This means that MyPy may fail to spot errors in some places, and may raise errors where none exist in other places. Dynamically constructing types at runtime is very useful in some situations, but goes against some fundamental principles of type-checking in Python, so you'll have trouble getting a type-checker to approve what you're doing without some kind of a hack.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
  • 2
    In my use case I have a class factory function that returns a class (`var=class_factory()`). This was the only approach I could find to get mypy to accept `var` as a class. Thanks! – JGC Dec 07 '21 at 20:55
0

Here is a related mypy issue 8897. In my case given this example:

Bla = type("Bla", (), {})  # type: ignore[valid-type, misc]        

class Foo(Bla):
    pass

I could convince mypy to accept Bla by explicitly typing the LHS of the assignment:

Bla: type = type("Bla", (), {})

Tested only with mypy 1.3.0 (compiled: yes).

Jens
  • 8,423
  • 9
  • 58
  • 78
-2

Will this work for you?

from typing import TypeVar, Optional

Foo = TypeVar('Foo')
Bar = Optional[Foo]
Peter Trcka
  • 1,279
  • 1
  • 16
  • 21
  • How does this fundamentally differ from the ``class Bar:`` approach already presented in the question? The part that makes this work is that it uses a ``class`` statement – adding a ``dataclass`` on top doesn't affect that. – MisterMiyagi Oct 08 '21 at 11:36
  • so the point here to create dynamic types, what is not easy if you have define type using class keyword? – Peter Trcka Oct 08 '21 at 11:44
  • 1
    Your edit still does nothing to address OP's original question, I'm afraid :/ `TypeVar` does *not* create a new class in the same way that calling `type` with three arguments does. [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar) documentation. [`type`](https://docs.python.org/3/library/functions.html#type) documentation. – Alex Waygood Oct 13 '21 at 09:58