SECTION 1
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.
1972 • C Language & Pointer Equality
== on pointers, making identity checks explicit but error-prone.1991 • Python 0.9 Released
is keyword for identity and == for value comparison, making the distinction readable in plain English.1995 • Java's .equals() Confusion
== for identity and .equals() for value comparison — the opposite convention from Python. This became a notorious source of bugs for beginners.2008 • Python 3 & Modern Best Practices
== 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.
SECTION 2
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.KEY TAKEAWAY
==), 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.SECTION 3
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.
SECTION 4
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.
IDENTITY CHECK
id() returns a unique integer for each living object. In CPython (the standard implementation), this integer is the object's memory address.
EQUALITY CHECK
Python calls the __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.
LOGICAL IMPLICATION
Identity always implies equality (same object means same value), but equality does not imply identity (equal values can live at different addresses).
Small Integer Caching
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.SECTION 5
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 |
Common == vs is results across different scenarios
SECTION 6
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.
Tracing == and is Through a Program
- 1
Step 1 — Create the first list
We writefruits = ["apple", "banana"]. Python allocates a new list object in memory and assigns it an identity (say, id = 4400). The variablefruitsnow references this object.fruits→ object at id 4400, value["apple", "banana"] - 2
Step 2 — Create a second list with the same contents
We writesnacks = ["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"] - 3
Step 3 — Test equality
We evaluatefruits == snacks. Python callsfruits.__eq__(snacks), which compares element by element: "apple" == "apple" and "banana" == "banana". All match, so the result isTrue.fruits == snacks→ True - 4
Step 4 — Test identity
We evaluatefruits is snacks. Python checks: is id(fruits) the same as id(snacks)? That's 4400 vs 5500 — they are different numbers.fruits is snacks→ False - 5
Step 5 — Create an alias
Now we writemenu = fruits. No new list is created! Python simply makesmenupoint to the same object thatfruitsalready references (id 4400). Nowmenu is fruitsis True, andmenu == fruitsis also True.menu is fruits→ True (same object) - 6
Step 6 — Observe aliasing side effect
We runmenu.append("cherry"). Sincemenuandfruitspoint to the same list, printingfruitsnow shows["apple", "banana", "cherry"]. Meanwhile,snacksis unaffected because it is a different object.fruits→["apple", "banana", "cherry"]— aliasing in action!
SECTION 7
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 |
Quick reference: choosing == vs is
GOLDEN RULE
== 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 ==.SECTION 8
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" |
How today's concepts connect to what you'll learn next
Looking Ahead
__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.SECTION 9
Practice Problems
Question 1: CONCEPTUAL
== checks and what is checks. Why can two objects be equal but not identical?Show answer
== operator checks whether two objects have the same value (equality), while is checks whether two variables point to the exact same object in memory (identity). Two objects can be equal but not identical because Python can create multiple objects that store the same data at different memory locations. For example, two lists with the same elements are still two separate objects. This is like having two identical copies of a book — they contain the same text, but they are physically separate books.Question 2: BASIC CALCULATION
a = [10, 20, 30]
b = [10, 20, 30]
c = a
print(a == b)
print(a is b)
print(a is c)
print(a == c)Show answer
a == b prints True because both lists contain the same elements. a is b prints False because each list literal creates a new object in memory. a is c prints True because c = a makes c an alias of the same object. a == c prints True because identity implies equality — if they are the same object, they must have the same value.Question 3: INTERMEDIATE
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.Show answer
x is y will print True because CPython caches integer objects in the range −5 to 256, so both variables reference the same cached object. For a is b with 257, the result may be True or False depending on how the code is executed — in a script, the compiler may optimize and reuse the object, but in an interactive REPL, two separate objects may be created. Relying on is for integers is bad practice because the caching behavior is an implementation detail of CPython and could change across versions or different Python implementations like PyPy or Jython. Always use == for value comparisons.Question 4: APPLIED
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.Show answer
result = find_user("Alice") followed by if result is None:. You should use is because None is a singleton — there is only one None object in all of Python. Using is is faster and avoids a subtle bug: if a custom class overrides __eq__ in a way that returns True when compared to None (which would be bad design but is technically possible), == None could incorrectly evaluate to True for an object that is not actually None. PEP 8, Python's official style guide, explicitly recommends is None over == None.Question 5: CRITICAL THINKING
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.Show answer
is is slightly faster (it only compares two integers), the classmate's argument confuses the direction of the implication. Identity implies equality, but equality does NOT imply identity. Using is as a replacement for == would produce incorrect results in common situations. For example: user_input = input() where the user types "hello". Comparing user_input is "hello" could return False because the string from input() is dynamically created and may not be the same object as the literal "hello", even though they contain the same characters. Similarly, [1, 2] is [1, 2] is always False. The tiny speed advantage of is is meaningless if it gives wrong answers. Correctness always comes before optimization.SUMMARY
Lesson Summary
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.