Rayan Hatout
Rayan Hatout

Reputation: 640

Understanding the behaviour of parametrizing a Generic with None

I am writing a fuzzer for a strongly-typed assembly-like language and I am relying on the Python typing system to parametrize the typing of the underlying generated language. I then inspect the Python types at runtime in order to dynamically generate the correct parametrization of the target fuzzed language.

I have a case where I parametrize a Generic[T, Optional[S]] and as the Optional hints, the S will sometimes be None. Let's take the following minimal example of class structure:

class Signed:
    ...

class Unsigned:
    ...

class OpTypeInt:
    ...

class OpTypeFloat:
    ...

class LogicalOperator(Generic[T, S]):
    ...

class BinaryLogicalOperatorFuzzMixin:
    ...

class BinaryLogicalOperator(LogicalOperator[T, Optional[S]]):
    ...

# "less than" OpCode parametrized for floats
class OpFLessThan(
    BinaryLogicalOperatorFuzzMixin, BinaryLogicalOperator[OpTypeFloat, None]
):
    ...

# "less than" OpCode parametrized for unsigned integers
class OpULessThan(
    BinaryLogicalOperatorFuzzMixin, BinaryLogicalOperator[OpTypeInt, Unsigned]
):
    ...

# "less than" OpCode parametrized for signed integers
class OpSLessThan(
    BinaryLogicalOperatorFuzzMixin, BinaryLogicalOperator[OpTypeInt, Signed]
):
    ...

Now during the runtime of the program, I will inspect these types to dictate the behaviour of the fuzzer. The inspection code looks like this:

base_type, type_constraint = get_args(OpSLessThan.__orig_bases__[1])
# OpSLessThan is a BinaryLogicalOperator[OpTypeInt, Signed]

print(base_type, type_constraint)
# <class '__main__.OpTypeInt'> <class '__main__.Signed'>

print(
    isinstance(type_constraint, Signed),
    type(type_constraint) == type(Signed),
    type(type_constraint) == Signed,
    type_constraint == type(Signed),
)
# False True False False

base_type, type_constraint = get_args(OpILessThan.__orig_bases__[1])
# OpILessThan is a BinaryLogicalOperator[OpTypeInt, None]

print(base_type, type_constraint)
# <class '__main__.OpTypeInt'> <class 'NoneType'>

print(
    isinstance(type_constraint, NoneType),
    type(type_constraint) == type(None),
    type(type_constraint) == None,
    type_constraint == type(None),
    type_constraint is None,
)
# False False False True False  huh?

Why is type(type_constraint) == type(Signed) a valid way of checking that the class has Signed as a base but isn't a valid way of checking for None? Why does None require to check for type_constraint == type(None)?

Upvotes: 0

Views: 111

Answers (1)

Paweł Rubin
Paweł Rubin

Reputation: 3390

TL;DR

Use issubclass:

base_type, type_constraint = get_args(OpFLessThan.__orig_bases__[1])
# OpILessThan is a BinaryLogicalOperator[OpTypeInt, None]

print(base_type, type_constraint)
# <class '__main__.OpTypeInt'> <class 'NoneType'>

print(issubclass(type_constraint, NoneType))
# True

type(a_class) == type(Signed) is not a valid way of checking that the class has Signed as a base.

It evaluates to True because type(<a class>) is type, hence type == type produces True.

>>> type(Signed)
<class 'type'>

This holds for every class in Python, including the type class itself:

>>> type(type)
<class 'type'>

type(None) != type(a_class) because None is an object, not a class, hence type(None) == NoneType.

Upvotes: 1

Related Questions