Search code examples
pythonpython-typing

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?


Solution

  • 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.