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
- type
- id and the is operator
- The small integer surprise
- Mutability, demonstrated
- Immutable objects
- Mutable objects
- The aliasing trap
- The mutable default argument
- isinstance and why it beats type
- Duck typing: often the better answer
- Hashability
- Common mistakes
- Best practices
- Practice
- Conclusion
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
Three questions about an object
| Question | Tool | Returns |
|---|---|---|
| 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) # Trueid 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? yesisasks "the same object?".==asks "the same value?". Useisonly forNone,True,Falseand 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 FalseCPython 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 idNothing 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) # unchangedThe 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 backThe 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 basketisinstance 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 ignoredPrefer 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]} # TypeErrorA tuple containing a list is itself unhashable, because its contents could still change. Hashability follows the contents all the way down.
Common mistakes
- Using
isto 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) == SomeClasswhereisinstancewas 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
isforNone,True,Falseand genuine identity checks. - Default a mutable argument to
Noneand 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
- Predict the output of
a = [1]; b = a; b += [2]; print(a)and then of the same code withb = b + [2]. Explain the difference. - Explain why
x = "hi"; y = "hi"; print(x is y)may printTrueand why you must not rely on it. - Write a function with a mutable default argument, demonstrate the bug across three calls, then fix it.
- Explain in one sentence why a list cannot be a dictionary key.
- Show a case where
isinstanceandtype(...) 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.