Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't Python NewType compatible with isinstance and type?

This doesn't seem to work:

from typing import NewType

MyStr = NewType("MyStr", str)
x = MyStr("Hello World")

isinstance(x, MyStr)

I don't even get False, but TypeError: isinstance() arg 2 must be a type or tuple of types because MyStr is a function and isinstance wants one or more type.

Even assert type(x) == MyStr or is MyStr fails.

What am I doing wrong?

like image 936
ragazzojp Avatar asked May 31 '26 11:05

ragazzojp


1 Answers

The purpose of NewType is purely for static type checking, but for dynamic purposes it produces the wrapped type. It does not make a new type at all, it returns a callable that the static type checker can see, that's all.

When you do:

x = MyStr("Hello World")

it doesn't produce a new instance of MyStr, it returns "Hello World" entirely unchanged, it's still the original str passed in, even down to identity:

>>> s = ' '.join(["a", "b", "c"])  # Make a new string in a way that foils interning, just to rule out any weirdness from caches
>>> ms = MyStr(s)  # "Convert" it to MyStr
>>> type(ms)       # It's just a str
str
>>> s is ms        # It's even the *exact* same object you passed in
True

The point is, what NewType(...) returns is effectively a callable that:

  1. Acts as the identity function (it returns whatever you give it unchanged); in modern Python, NewType itself is a class that begins with __call__ = _idfunc, that's literally just saying when you make a call with an instance, return the argument unchanged.
  2. (In modern Python) Has some useful features like overloading | to produce Unions like other typing-friendly things.

but you can't use it usefully for isinstance, not because it's not producing instances of anything.

As other answers have mentioned, if you need runtime, dynamic checking, subclassing is the way to go. The other answers are doing both more (unnecessarily implementing __new__) and less (allowing arbitrary attributes, bloating instances of the subclass for a benefit you won't use) than necessary, so here's what you want for something that:

  1. Is considered a subclass of str for both static and runtime checking purposes
  2. Does not bloat the memory usage per-instance any more than absolutely necessary
class MyStr(str):   # Inherit all behaviors of str
    __slots__ = ()  # Prevent subclass from having __dict__ and __weakref__ slots, saving 16 bytes
                    # per instance on 64 bit CPython builds, and avoiding weirdness like allowing
                    # instance attributes on a logical str

That's it, just two lines (technically, a one-liner like class MyStr(str): __slots__ = () is syntactically legal, but it's bad style, so I avoid it), and you've got what you need.

like image 165
ShadowRanger Avatar answered Jun 02 '26 01:06

ShadowRanger