Mutability, Identity and Type Checking

type() names the class, id() names the object and isinstance() answers the question you usually meant to ask. Together they explain every aliasing surprise in Python.

Three questions about an object

QuestionToolReturns
What kind of thing is this?type(x)The class object
Which object is this, exactly?id(x)A number unique to that object while it lives
Can I treat this as a kind of thing?isinstance(x, T)True or False

type

print(type(5))                 # <class 'int'>
print(type(5).__name__)        # int

value = [1, 2]
print(type(value) is list)     # True

id and the is operator

id(x) returns a number that identifies the object for as long as it exists. In CPython it happens to be the memory address, but that is an implementation detail; what matters is that two names sharing an id are two names for one object.

a = [1, 2, 3]
b = a                # same object
c = [1, 2, 3]        # a different object with equal contents

print(id(a) == id(b))    # True
print(id(a) == id(c))    # False

print(a is b)            # True   - same object?
print(a is c)            # False  - same object? no
print(a == c)            # True   - equal contents? yes
is asks "the same object?". == asks "the same value?". Use is only for None, True, False and deliberate identity checks. Use == for everything else.

The small integer surprise

a = 256
b = 256
print(a is b)     # True

a = 257
b = 257
print(a is b)     # often False

CPython pre-creates the integers from -5 to 256 and reuses them, so identity accidentally matches for small values. This is an optimisation, not a rule of the language, and relying on it is how a program breaks on a different interpreter. It is also a favourite interview question, which is the main reason to know it.

Mutability, demonstrated

Immutable objects

text = "hello"
print(id(text))

text += " world"      # builds a NEW string
print(id(text))       # a different id

Nothing was modified. A new string was built and the name was rebound. The original "hello" is untouched, which is exactly why strings are safe to share.

Mutable objects

items = [1, 2]
print(id(items))

items += [3]          # modifies the list in place
print(id(items))      # the SAME id
print(items)          # [1, 2, 3]

Same object, changed contents. This asymmetry between str and list for the identical += operator is worth pausing on, because it drives the next section.

The aliasing trap

original = {"a": 1}
copy_attempt = original          # NOT a copy

copy_attempt["b"] = 2
print(original)                  # {'a': 1, 'b': 2}
shallow = original.copy()        # a real, independent dictionary
shallow["c"] = 3
print(original)                  # unchanged

The mutable default argument

This is the classic Python trap, and it is pure mutability.

def add_item(item, basket=[]):       # WRONG
    basket.append(item)
    return basket


print(add_item("pen"))       # ['pen']
print(add_item("book"))      # ['pen', 'book'] - the same list came back

The default value is created once, when the def statement runs, not on each call. Every call that omits the argument shares that one list. The fix is always the same:

def add_item(item, basket=None):     # correct
    if basket is None:
        basket = []
    basket.append(item)
    return basket

isinstance and why it beats type

print(isinstance(5, int))              # True
print(isinstance(5, (int, float)))     # True, any of these types
print(isinstance(True, int))           # True, because bool subclasses int

print(type(True) is int)               # False - the subclass is ignored

Prefer isinstance. It respects inheritance, which is almost always what you want. Reach for type(x) is T only when you specifically need to exclude subclasses.

Duck typing: often the better answer

def total_length(items):
    return sum(len(item) for item in items)

This function does not check any type. It works for anything whose elements support len: strings, lists, tuples, sets, dictionaries. Checking types up front would have narrowed it for no benefit. The Python habit is to describe what an object must be able to do, not what class it must belong to.

Hashability

Immutability has one hard consequence: only hashable objects can be dictionary keys or set members, and mutable built ins are not hashable.

print(hash("abc"))          # fine
print(hash((1, 2)))         # fine, a tuple of immutables
# print(hash([1, 2]))       # TypeError: unhashable type: 'list'

seen = {(1, 2), (3, 4)}     # a set of tuples: fine
# seen = {[1, 2]}           # TypeError

A tuple containing a list is itself unhashable, because its contents could still change. Hashability follows the contents all the way down.

Common mistakes

  • Using is to compare values. It works by accident for small integers and short strings, then fails on real data.
  • Assuming assignment copies a container.
  • Using a mutable default argument.
  • Using type(x) == SomeClass where isinstance was meant.
  • Trying to use a list or a dictionary as a dictionary key.
  • Believing id() values are stable across runs. They are not, and nothing should depend on them.

Best practices

  • Reserve is for None, True, False and genuine identity checks.
  • Default a mutable argument to None and build the real value inside the function.
  • Ask what an object can do before asking what it is; type checks are a last resort.
  • Prefer immutable types for anything shared, cached or used as a key.

Practice

  1. Predict the output of a = [1]; b = a; b += [2]; print(a) and then of the same code with b = b + [2]. Explain the difference.
  2. Explain why x = "hi"; y = "hi"; print(x is y) may print True and why you must not rely on it.
  3. Write a function with a mutable default argument, demonstrate the bug across three calls, then fix it.
  4. Explain in one sentence why a list cannot be a dictionary key.
  5. Show a case where isinstance and type(...) is ... disagree, and say which answer you wanted.

Conclusion

== compares values and is compares objects; mutable objects can change under a shared name while immutable ones cannot; and only immutable objects can be keys. Every aliasing bug in Python is one of those three sentences applied without thinking.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.