Python • Syntax and Core Types

Equals vs. Is — Distinguish == from is (equality vs identity) (conceptual)

Learn why two things can look the same yet not be the same object in Python's memory.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 8 figures

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

The C programming language required programmers to compare memory addresses directly with == on pointers, making identity checks explicit but error-prone.

1991 • Python 0.9 Released

Guido van Rossum released Python with the is keyword for identity and == for value comparison, making the distinction readable in plain English.

1995 • Java's .equals() Confusion

Java used == 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

Python 3 refined how objects are stored and compared, but the core distinction between == 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.

1

Equality (==)

The == 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.
2

Identity (is)

The 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.
3

The id() Function

Python's built-in id() function returns an integer representing an object's unique identity. When id(a) == id(b), then a is b is True.
4

Identity Implies Equality

If two variables point to the same object (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

Think of it like identical twins. Two twins may look the same (equality, ==), 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

Python Memory Model: Equality vs IdentityVariablesaref → 0x1Abref → 0x2Bcref → 0x1AMemory (Heap)id: 0x1A[1, 2, 3]id: 0x2B[1, 2, 3]a == b → True ✓Same value [1, 2, 3] — different objectsa is b → False ✗a is c → True ✓Same object at address 0x1Aa == c → True ✓ (always)Identity (is) implies Equality (==), but Equality does NOT imply Identity
Variables 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

aisb⟺id(a)==id(b)a is b ⟺ id(a) == id(b)

id() returns a unique integer for each living object. In CPython (the standard implementation), this integer is the object's memory address.

EQUALITY CHECK

a == b ⟺ a.__eq__(b) returns True

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

(aisb)→(a==b)but(a==b)↛(aisb)(a is b) → (a == b) but (a == b) ↛ (a is b)

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

Python pre-creates and reuses integer objects for values −5 through 256. This means 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.

== vs is Behavior by TypeSmall Integers (−5 to 256)x = 10y = 10x == y → Truex is y → TrueCached / InternedBoth always TrueLarge Integers / Floatsx = 1000y = 1000x == y → Truex is y → ???Not guaranteed cachedUnreliable for isLists / Dictsx = [1, 2]y = [1, 2]x == y → Truex is y → FalseAlways separate objectsNever use is hereNone (Singleton)x = Nonex is None → TrueOnly one None object exists✓ Best practice: always use "is None"Strings (Sometimes Interned)x = "hello"y = "hello"x == y → Truex is y → usually True**Implementation detail — use == for strings
This chart shows how five categories of Python types behave with == 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== Resultis ResultWhy?
[1,2] == [1,2]TrueFalseTwo separate list objects with equal contents
a = b = [1,2]TrueTrueBoth names reference the same single list
"hi" == "hi"TrueTrue*Short strings are often interned (*not guaranteed)
x is None—True/FalseOnly one None exists; this is the recommended check
256 is 256TrueTrueIn the cached range (−5 to 256)
257 is 257True???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. 1

    Step 1 — Create the first list

    We write 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"]
  2. 2

    Step 2 — Create a second list with the same contents

    We write 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"]
  3. 3

    Step 3 — Test equality

    We evaluate 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 → True
  4. 4

    Step 4 — Test identity

    We evaluate fruits 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. 5

    Step 5 — Create an alias

    Now we write menu = 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)
  6. 6

    Step 6 — Observe aliasing side effect

    We run 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!

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.

ScenarioUse ==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

Think of == 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 LessonAdvanced 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 objectShallow vs. Deep Copy: copy.copy() creates a new container but reuses inner objects; copy.deepcopy() recursively copies everything
Small integer cachingDesign Patterns: the Flyweight and Singleton patterns intentionally share objects to save memory, similar to Python's internal caching
x is None idiomSentinel 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

In more advanced courses you will encounter hash-based data structures like dictionaries and sets. These rely on the rule that objects considered equal (__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

In your own words, explain the difference between what == checks and what is checks. Why can two objects be equal but not identical?
Show answer
The == 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

Predict the output of each line: 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

Consider this code: 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

You are building a function that returns 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
The correct check is: 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

A classmate argues: "Since identity implies equality, and 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
While it is true that 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.

Varsity Tutors • Python • Equals vs. Is — Distinguish == from is (equality vs identity) (conceptual)