2
\$\begingroup\$

Background

The typing module supports subscriptable types. Here is an example use case:

from typing import List def foo(bar: List[int]): pass 

However, when you try to use this with isinstance, it fails:

>>> from typing import List >>> my_list = [1, 2, 3] >>> isinstance(my_list, List[int]) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 719, in __instancecheck__ return self.__subclasscheck__(type(obj)) File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 722, in __subclasscheck__ raise TypeError("Subscripted generics cannot be used with" TypeError: Subscripted generics cannot be used with class and instance checks 

Code

So, I wrote a program which can test for subscripted types. Here is my code:

class Union: def __init__(self, type1, type2): if not isinstance(type1, (type, Type)) or not isinstance(type2, (type, Type)): raise ValueError(f'{type1!r}, {type2!r} are not types') self.types = type1, type2 def __str__(self): return self.types[0].__name__ + ' | ' + self.types[1].__name__ def __repr__(self): return str(self) @property def __name__(self): return str(self) class Type: def __init__(self, dtype, of=None): if isinstance(dtype, Type): self.dtype = dtype.dtype elif isinstance(dtype, (type, Union)): self.dtype = dtype else: raise ValueError(f'{dtype!r} is not a type') if of is not None and not isinstance(of, (Type, Union, type)): raise ValueError(f'{of!r} is not a type') self.of = of def __str__(self): if self.of is not None: return f'{self.dtype.__name__}[{str(self.of.__name__)}]' else: return str(self.dtype.__name__) def __repr__(self): return str(self) def __getitem__(self, cls): return Type(self, cls) def __or__(self, other): return Union(self, other) def __ror__(self, other): return Union(other, self) @property def __name__(self): return str(self) def IsInstance(obj, dtype): if isinstance(dtype, type): return isinstance(obj, dtype) elif isinstance(dtype, Type): if dtype.of is None: return isinstance(obj, dtype.dtype) return all(IsInstance(item, dtype.of) for item in obj) elif isinstance(dtype, Union): return any(IsInstance(obj, t) for t in dtype.types) Bool = Type(bool) Dict = Type(dict) Float = Type(float) Int = Type(int) List = Type(list) Set = Type(set) Str = Type(str) Tuple = Type(tuple) 

Notes

  • Union is the equivalent of typing.Union, but it is a bit different. You can get a Union by either:
    • Using Union(int, float)
    • Using Int | Float
  • To do the instance check, use IsInstance, not isinstance. For example:
    • Use IsInstance([1, 2, 3], List[int])

The code is also on Github, and PyPI

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

I get the motivation for doing this, but it crosses an important line - one that I've myself crossed and then somewhat regretted later.

There's only so much you can do to save Python from itself. The type system is weak, and will never be strong no matter how clever we attempt to be. PEP484 is explicitly about static type analysis and not runtime type enforcement.

You've implemented IsInstance to check for the hint, but crucially it never actually checks whether the associated data in runtime match the hint.

Also you've written Type, Union, Dict, List, Set and Tuple classes that, while they don't technically shadow their alternatives in the built-in typing module, are really not a good idea to name identically.

Please don't do any of this. Use of isinstance itself is very often an anti-pattern, but we can't say for sure because you haven't shown where this code is used. Frequent remedies to isinstance are class polymorphism, writing type-alternative front-end functions to a core logic function (since Python has no signature overloading), etc.

\$\endgroup\$
2
  • \$\begingroup\$ "You've implemented IsInstance to check for the hint, but crucially it never actually checks whether the associated data in runtime match the hint." - I didn't want to do that. If I want to check the type, I'll do it explicitly by calling IsInstance. \$\endgroup\$ Commented Oct 15, 2022 at 16:42
  • \$\begingroup\$ You're doing something that should be done by mypy and not you, in static time and not runtime, and you're failing to actually validate runtime data. So I think what you want to do is a problem. \$\endgroup\$ Commented Oct 15, 2022 at 20:39
3
\$\begingroup\$

Initially, I just wanted to leave a confirmation on Reinderien's answer, but since I recently saw myself on a similar situation as the person who raised the topic, I wanted to elaborate on why I agree, based on an example that depicts such situation.

I was recently thinking about data validation of Class instances while using dataclasses on Python 3.12. The example below, works in a very primitive way, specially because it's not inspecting/validating the content of the container/data structure.

Still, it just validates at Class instantiation level, not implementing a full runtime type enforcement/validation:

#!/usr/bin/python3.12 from dataclasses import dataclass, field @dataclass class User: username: str skills: list[str] = field(default_factory=list) def validation(self) -> None: """ Inspecting __dataclass__fields, obtaining direct or original data type, verifying if field value has the expected data type. """ wrong_data_types = [] for field in self.__dataclass_fields__.keys(): data_type_detected = getattr(self.__dataclass_fields__[field].type, "__origin__", self.__dataclass_fields__[field].type) if not isinstance(getattr(self, field), data_type_detected): wrong_data_types.append(field) if wrong_data_types: raise AttributeError(f"data type violation detected: {wrong_data_types}") def __post_init__(self): self.validation() if __name__ == "__main__": # username must be a str... user = User(username=123, skills=["docker", "aws", "azure"]) 

After correctly instantiating the Class, you could change the instance variables to another data type:

user = User(username="jack62", skills=["docker", "aws", "azure"]) user.username = 123 

About __origin__ and Parameterized Collections

list[int] is a types.GenericAlias object, containing __origin__ attribute, which points at the non-parameterized generic class, then helping on the data validation proposal presented before.

PEP-585 (Python 3.9) talks more about this and other attributes, besides explaining more about Parameterized Collections and the error mentioned on the question.

A better validation (but not enough)

Pydantic, it's a similar (with more powerful validations):

from pydantic import BaseModel class User(BaseModel): id: int name: str external_data = { 'id': 48, 'name': 'jack62', } user = User(**external_data) print("name (before): ", user.name) user.name = 123 print("name (after): ", user.name) 

Still, validation and type enforcement happens at instantiation level of the Class, but not ensuring it for the rest of your program's runtime:

$ python3 test_pydantic.py name (before): jack62 name (after): 123 

Crossing this line, indeed, seems to be a complicated thing to tackle...

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.