Historical Context & Motivation
When programming languages were first designed, engineers had to decide how a computer should answer a seemingly simple question: are these two things "the same"? In everyday life, you might say two identical-looking sneakers are "the same," but they are still two separate shoes. Early languages like C and Assembly forced programmers to think carefully about whether they meant "same value" or "same location in memory." Python, created by Guido van Rossum in 1991, inherited this distinction and gave it a clean, readable syntax with two separate operators: == for equality and is for identity.
== on pointers, making identity checks explicit but error-prone.is keyword for identity and == for value comparison, making the distinction readable in plain English.== for identity and .equals() for value comparison — the opposite convention from Python. This became a notorious source of bugs for beginners.== and is remained fundamental to the language.The core question this lesson answers is straightforward yet surprisingly tricky: when you write a == b, Python checks whether a and b hold the same value, but when you write a is b, Python checks whether they are literally the same object in memory. Understanding when and why these give different answers is a key milestone in becoming a confident Python programmer.
Core Principles & Definitions
Every object you create in Python lives somewhere in the computer's memory. Python assigns each object a unique identity — essentially an address that never changes for the lifetime of that object. Two variables can point to the same address (they are identical), or they can point to different addresses that happen to store equal data (they are equal). The following principles clarify this distinction.
Equality (==)
== operator compares the values of two objects. If two lists contain the same elements in the same order, == returns True, even if they are stored at different memory locations.Identity (is)
is operator compares the identities (memory addresses) of two objects. It returns True only when both variables point to the exact same object — not just an equivalent copy.The id() Function
id() function returns an integer representing an object's unique identity. When id(a) == id(b), then a is b is True.Identity Implies Equality
a is b is True), then a == b must also be True. However, the reverse is not guaranteed — equal values can live at separate addresses.==), but they are still two different people (not identical, is returns False). If you are pointing at one specific twin, then is only returns True when someone else points at that exact same person — not the other twin.Visual Explanation — Memory Model
a and c both reference the same list object at address 0x1A (the pink dashed arrow and cyan solid arrow land on the same box). Variable b points to a separate list at 0x2B that contains the same values. Both boxes hold [1, 2, 3], so a == b is True, but a is b is False.The diagram above is the single most important mental model for this lesson. Notice how a and c share an arrow landing on the same box — they are aliases for the same object. Modifying the list through a will also change what c sees, because they reference one shared object. Meanwhile, b is independent — it is an equal copy, not the same thing. This distinction matters enormously when you start passing lists or dictionaries to functions.
How Python Decides Under the Hood
When Python encounters the is operator, it does something remarkably simple: it compares the integer IDs of the two operands. You can think of this as checking whether two postal addresses are the same number. The == operator, on the other hand, calls a special method called __eq__ on the left operand, which lets each type define what "equal" means. For lists, equality means same length and same elements in the same order. For strings, it means the same characters. For custom objects, the programmer can define it however they want.
id() returns a unique integer for each living object. In CPython (the standard implementation), this integer is the object's memory address.__eq__ method, which each type can override. The default behavior (inherited from object) falls back to identity: a == b is the same as a is b unless the class defines otherwise.x = 100; y = 100; x is y returns True because both variables point to the same cached object. However, x = 1000; y = 1000; x is y may return False because 1000 is outside the cache range and Python may create two separate objects. This is an implementation detail — never rely on it.Behavior Across Python Types
Different Python types behave differently with == and is because of how Python manages objects internally. Some objects are interned (shared automatically to save memory), while others are always created fresh. Understanding which types may be interned helps you predict when is and == give the same result — and, more importantly, when they do not.
== and is. The green results are reliable; the red and yellow results are either always False or unpredictable. The golden rule: use is only for None, True, and False; use == for everything else.| Expression | == Result | is Result | Why? |
|---|---|---|---|
[1,2] == [1,2] | True | False | Two separate list objects with equal contents |
a = b = [1,2] | True | True | Both names reference the same single list |
"hi" == "hi" | True | True* | Short strings are often interned (*not guaranteed) |
x is None | — | True/False | Only one None exists; this is the recommended check |
256 is 256 | True | True | In the cached range (−5 to 256) |
257 is 257 | True | ??? | Outside cache — depends on Python version and context |
Worked Example — Tracing Identity and Equality
Let's trace through a short Python program step by step, predicting the output of each comparison. This exercise builds the habit of thinking about what Python is actually doing in memory.
fruits = ["apple", "banana"]. Python allocates a new list object in memory and assigns it an identity (say, id = 4400). The variable fruits now references this object.fruits → object at id 4400, value ["apple", "banana"]snacks = ["apple", "banana"]. Python creates a brand-new list object (id = 5500). Even though it contains the same elements, it is a separate object.snacks → object at id 5500, value ["apple", "banana"]fruits == snacks. Python calls fruits.__eq__(snacks), which compares element by element: "apple" == "apple" and "banana" == "banana". All match, so the result is True.fruits == snacks → Truefruits is snacks. Python checks: is id(fruits) the same as id(snacks)? That's 4400 vs 5500 — they are different numbers.fruits is snacks → Falsemenu = fruits. No new list is created! Python simply makes menu point to the same object that fruits already references (id 4400). Now menu is fruits is True, and menu == fruits is also True.menu is fruits → True (same object)menu.append("cherry"). Since menu and fruits point to the same list, printing fruits now shows ["apple", "banana", "cherry"]. Meanwhile, snacks is unaffected because it is a different object.fruits → ["apple", "banana", "cherry"] — aliasing in action!When to Use == and When to Use is
Now that you understand the difference, the practical question is: which one should you reach for in your code? The answer is simpler than you might expect. In the vast majority of cases, == is the right choice. There are only a few situations where is is preferred, and they all involve Python's built-in singleton objects — objects where only one instance ever exists.
| Scenario | Use == | Use is |
|---|---|---|
| Comparing numbers | ✅ Always — x == 42 | ❌ Unreliable due to caching |
| Comparing strings | ✅ Always — name == "Alice" | ❌ Interning is not guaranteed |
| Comparing lists, dicts, sets | ✅ Checks contents — a == b | ⚠️ Only if you truly need aliasing check |
| Checking for None | ⚠️ Works but not Pythonic | ✅ Best practice — x is None |
| Checking for True/False | ⚠️ Works but can have edge cases | ✅ Preferred — x is True |
| Sentinel values (custom markers) | ❌ Another object could be "equal" | ✅ Ensures the exact marker object |
== as your everyday tool — it is the screwdriver you reach for 95% of the time. The is operator is a specialty tool: use it only when comparing to None, True, or False (the singletons). If you are comparing values of numbers, strings, lists, or any other data, always use ==.Connection to Advanced Concepts
Understanding equality vs. identity is your gateway to several advanced Python topics. When you learn about mutable vs. immutable types, you will see that mutability makes aliasing dangerous: if two variables share a mutable object (like a list), changing one affects the other. With immutable objects (like strings, tuples, and integers), aliasing is harmless because you cannot change the object in place. This is also the foundation for understanding shallow copies vs. deep copies, Python's copy module, and why functions that receive mutable arguments can produce unexpected side effects.
| This Lesson | Advanced Topic |
|---|---|
is checks identity (same object) | Object-Oriented Programming: every instance has a unique identity, and you can override __eq__ to define custom equality |
| Aliasing: two names, one object | Shallow vs. Deep Copy: copy.copy() creates a new container but reuses inner objects; copy.deepcopy() recursively copies everything |
| Small integer caching | Design Patterns: the Flyweight and Singleton patterns intentionally share objects to save memory, similar to Python's internal caching |
x is None idiom | Sentinel pattern in APIs: libraries create unique sentinel objects and check with is to distinguish "no value given" from "None was given" |
__eq__ returns True) must produce the same hash value. If you ever build a custom class, you must keep __eq__ and __hash__ in sync — a lesson rooted directly in today's equality vs. identity distinction.Practice Problems
== checks and what is checks. Why can two objects be equal but not identical?a = [10, 20, 30]
b = [10, 20, 30]
c = a
print(a == b)
print(a is b)
print(a is c)
print(a == c)x = 256
y = 256
print(x is y)
a = 257
b = 257
print(a is b)
Explain why these two blocks could produce different results, and explain why relying on is for integer comparison is a bad practice.None when a user is not found in a database, and returns a user dictionary otherwise. Write an if statement that correctly checks whether the function returned None. Explain why you chose == or is, and describe a scenario where using the wrong operator could cause a bug.is is faster than ==, we should always use is instead of ==." Write a thoughtful response that addresses their logic and provides a concrete code example showing why this reasoning fails.Lesson Summary
Python provides two distinct comparison mechanisms. The == operator tests equality — whether two objects hold the same value — by calling the __eq__ method. The is operator tests identity — whether two variables reference the exact same object in memory by comparing their id() values. Identity always implies equality, but equality does not imply identity.
In practice, use == for all value comparisons — numbers, strings, lists, dictionaries, and any other data. Reserve is exclusively for checking against singletons like None, True, and False. Be aware of Python's small integer caching (−5 to 256) and string interning as implementation details that should never be relied upon. Understanding this distinction is foundational for working with mutable objects, aliasing, and copying in Python.