Table of Contents
In Python, the identity operators is
and is not
are used to compare memory locations of two objects - not just their values. This makes them different
from the equality operators (==
and !=
), which check if two values are equal. Identity operators are particularly useful when dealing with immutable
data types, singleton objects like None
, or comparing references in data structures. Understanding identity logic is essential when working with object-oriented
programming, function arguments, or performance optimization in Python.
The practice tasks below will help you understand how identity operators work with lists, strings, numbers, and built-in constants. By solving these problems, you'll gain clarity on how Python manages memory and how object comparison works behind the scenes.
To successfully complete the tasks in this section, you should review the following topics:
- Identity Operators in Python
- Numeric Data Types
- Sequence Types (for understanding mutable and immutable objects)
- Variables and Object References
- Difference between equality (
==
) and identity (is
) - Basics of memory allocation in Python
Beginner-Level Practice – Identity Operators
At the beginner level, your goal is to understand the difference between object equality and identity. You’ll explore how Python handles variable assignments and compares object
references in memory. These tasks will help you see that two variables may look the same in value but still not be the same in memory. This is a common point of confusion for
beginners who assume ==
and is
are interchangeable. Mastering this difference early on will make debugging and writing safe conditionals easier in the
future.
Excercise 1. Comparing Lists and Copies
Write a program that creates a list of fruits and assigns it to a second variable. Then, make a shallow copy of the list using slicing. Use is
and
==
to compare the original list, the second reference, and the sliced copy. Print the results and explain what is equal and what is identical.
What this Task Teaches
This task highlights the key difference between object identity and value equality. You'll learn that two lists may have the same content but reside in different places in
memory. You'll also practice using is
and ==
correctly in logical checks when working with data containers like lists.
Hints and Tips
Assign the list directly to one variable, then use slicing like [:]
to make a shallow copy. Use ==
to check if contents are equal and
is
to test whether they share the same identity.
- Use the
id()
function to display memory locations - Compare the original and copied lists with
==
andis
- Print descriptive messages to make your output clear
Common Mistakes
Beginners often confuse ==
with is
. While ==
checks if the values are the same, is
checks whether both sides point to the
same object in memory. Using is
to compare values can result in logic bugs, especially when working with mutable data like lists and dictionaries.
- Assuming copy is identical: Using slicing
[:]
creates a new list - it’s equal in value, but not the same object. -
Using
is
for value checks: This works only in rare cases, like small integers or cached strings. - Not printing or labeling comparisons: Without clear print messages, it's hard to know what you're testing.
-
Skipping the
id()
function:id()
helps visualize the identity of each object. - Confusing behavior with mutability: Changes to one object might affect another only if they are truly identical.
Step-by-Step Thinking
Begin by creating a list and assigning it to two different variables - one by reference, one by slicing. Compare the three and check their behavior.
- Create a list:
fruits = ['apple', 'banana']
- Assign it to
same_list = fruits
- Copy it with slicing:
copied_list = fruits[:]
- Compare
fruits == same_list
andfruits is same_list
- Compare
fruits == copied_list
andfruits is copied_list
- Use
id()
to display memory locations of each list - Print conclusions about identity vs. equality
How to Make it Harder
Try changing one element in each list and see which lists reflect the change. This reveals the behavior of references vs. independent copies.
- Modify an item in
same_list
and check iffruits
is also updated - Modify
copied_list
and observe if it affects the original - Try using
copy()
method from thecopy
module
Excercise 2. Is None or Not?
Ask the user for their age. If they press Enter without typing anything, store None
as their value. Then check whether the input is None
using the
is
operator. Print an appropriate message depending on whether the user provided an input or skipped it.
What this Task Teaches
This task teaches you how to use the is
operator when working with special singleton values like None
. It demonstrates the correct and safe way to
check for the absence of a value - a pattern commonly used in data validation, APIs, and input parsing in Python.
Hints and Tips
Use an if not age_input
check to determine if the string is empty. If it is, assign None
. Then compare using is None
- not
== None
.
- Use
input()
to ask for age - Check if the response is empty:
if not value
- If empty, assign
age = None
- Then use
if age is None
for your condition
Common Mistakes
A very common mistake is using == None
instead of is None
. While this may work in some cases, the recommended and safer approach is always to use
identity comparison with singletons like None
, True
, and False
.
-
Using
== None
: Python recommends usingis
forNone
checks. - Comparing to the wrong type: Comparing empty strings directly to
None
can cause confusion. - Incorrect assignment: Forgetting to assign
None
explicitly can make checks fail silently. - Mixing value checks and identity checks: You must decide whether you want to compare content or object.
-
Overusing
is
elsewhere: Don’t start usingis
for strings or integers - it doesn’t always behave as expected.
Step-by-Step Thinking
Think of the logic as: “Did the user enter a value or not?” If not, use None
. Then test the presence or absence with is
.
- Use
input()
to get user input - Check if input is empty - use
if not age_str
- Assign
None
if no input, else convert toint
- Use
if age is None
to check condition - Print message based on whether age was entered or not
How to Make it Harder
Use identity comparison in a loop that keeps asking for input until a valid number is entered. Add validation and re-prompt logic.
- Repeat input prompt until age is not
None
- Add a message saying “Age is required!”
- Handle invalid numeric conversion using
try/except
Intermediate-Level Practice – Identity Operators
At the intermediate level, you'll dive deeper into how Python handles memory and references when working with custom objects, immutable values, and built-in constants. These
tasks will make you think more carefully about how assignments affect identity and how the is
and is not
operators behave in different contexts. You’ll
also explore scenarios where identity checks are more reliable than equality checks - particularly when working with None
, booleans, and singletons. Understanding
these nuances will prepare you for cleaner code and fewer logic bugs in conditional logic and function arguments.
Excercise 1. Shared Config or Not?
Create a simple class called Config
with one attribute. Then make two variables that point to the same Config
object. Create a third variable that is
a separate but identical Config
instance. Write comparisons using both is
and ==
and explain which objects share memory identity and
which ones only match in value.
What this Task Teaches
This task demonstrates how user-defined objects behave with identity comparisons. It also teaches the difference between assigning the same reference vs. creating new instances. You’ll get a hands-on understanding of how classes and object memory addresses interact in Python.
Hints and Tips
Use a constructor (__init__
) to set an attribute like theme
. Don’t override __eq__
- just test the defaults.
- Use
cfg1 = Config()
andcfg2 = cfg1
to share a reference - Use
cfg3 = Config()
to create a new instance - Compare
cfg1 is cfg2
andcfg1 is cfg3
- Also test
cfg1 == cfg3
and observe the result
Common Mistakes
Many developers mistakenly think that two objects with identical attributes are the same in memory. Unless they point to the exact same instance, Python treats them as separate objects. Forgetting this can cause bugs in caching, configuration sharing, or object tracking.
-
Misinterpreting
==
as identity: Two objects can have equal attributes but still be different in memory. - Overwriting references: Accidentally assigning one variable over another may break shared logic.
-
Not printing
id()
: It's crucial to visualize actual memory references when debugging. - Modifying one instance and expecting another to change: Only shared references will reflect the update.
- Assuming all instances behave like strings: Unlike immutable primitives, class instances behave differently.
Step-by-Step Thinking
Focus on creating two references to the same object, and one to a new object. Then compare both identity and equality.
- Define a class
Config
with atheme
attribute - Assign
cfg1 = Config()
and thencfg2 = cfg1
- Create another:
cfg3 = Config()
- Use
is
to compare cfg1/cfg2 and cfg1/cfg3 - Use
==
to compare them as well - Print
id()
of all instances - Print conclusion: which share memory and which don’t
How to Make it Harder
Add a method that modifies the instance and observe which objects reflect the change. Try defining __eq__
manually to make equality work across objects.
- Add
def set_theme(self, theme):
and update the theme - Modify
cfg1
and observe changes incfg2
andcfg3
- Override
__eq__
to allow equality by value
Excercise 2. None or Not? Handling Default Arguments
Create a function that takes an optional argument status=None
. Inside the function, check if the user passed any value by comparing status is None
.
Return a default message if no status is given. Otherwise, return a customized message. Then, test the function by calling it with different inputs.
What this Task Teaches
This task demonstrates how is
is used in function arguments to check if the caller provided a value. It teaches the correct way to handle default parameters and
prevent logical errors when None
is a valid argument. This is a common pattern used in real-world APIs and libraries.
Hints and Tips
Use status is None
to determine whether to use a default response or a user-provided value. Remember, don’t check with ==
.
- Define the function like:
def greet(status=None):
- Inside the function:
if status is None:
- Return “No status provided” or something similar
- If a status is passed, return “Status: X”
- Call function with and without arguments to test
Common Mistakes
Developers often forget that None
must be compared using is
. Using ==
might work but can break if custom objects override comparison
logic. Also, beginners may accidentally use mutable defaults instead of None
.
-
Using
== None
instead ofis None
: This is unreliable when custom__eq__
is defined. - Returning nothing: Forgetting to return a value causes
None
to be printed automatically. - Using mutable default args: Never use
[]
or{}
as default arguments. - Not testing both conditions: Always test with both passed and missing arguments.
- Incorrect print formatting: Forgetting f-strings or
+
concatenation can break your message.
Step-by-Step Thinking
You need to detect whether the function was called with or without arguments. Use identity comparison for this.
- Define the function with
status=None
in signature - Inside, use
if status is None
to detect missing argument - Return a default string when nothing is passed
- Return a formatted string when value is passed
- Test the function with and without arguments
How to Make it Harder
Try allowing None
to mean “unknown” and check for False
, ""
, and other falsy values. Refactor logic with a sentinel object instead of
None
.
- Use a custom object as the default argument (e.g.,
_UNSET = object()
) - Detect true absence of a value, not just
None
- Add support for additional value types (bool, str, etc.)
Advanced-Level Practice – Identity Operators
At the advanced level, identity operators are no longer just tools for comparing simple variables - they become part of logic validation in performance-critical systems, memoization, and singleton management. These tasks will challenge your understanding of how Python stores and reuses small objects like integers, strings, and tuples. You'll also explore cases where the `is` operator can produce misleading results due to internal optimizations. These exercises simulate real-world use cases like config caching and object pooling - scenarios where identity checks are more efficient than equality checks.
Excercise 1. Is That the Same Tuple?
You are given a function that returns the same tuple of default settings used in different parts of your application. Your task is to verify whether two returned tuples from two different calls refer to the same object in memory using identity comparison. Then, simulate a modification in one function call and check if the change affects the second.
What this Task Teaches
This task reveals how Python handles immutable data types and how tuple interning or caching can affect the result of identity comparison. You'll see how even when objects look the same, their memory references might differ - unless Python reuses them for optimization. Understanding when this reuse happens is key to writing high-performance code and debugging memory-related issues.
Hints and Tips
Tuples are immutable, so you can't modify them directly - but you can replace them. Python sometimes caches small, identical tuples. Pay attention to what values are stored and returned.
- Define a function
get_default_settings()
that returns a tuple like("dark", True, 10)
- Call the function twice and compare the results using
is
- Use
id()
to show memory reference - Try modifying the returned tuple by assigning a new one
- Check if other references are still identical
Common Mistakes
Developers may incorrectly assume that small immutable structures like strings or tuples are always the same object when they contain the same data. This can lead to bugs in cache validation or lazy evaluation logic. Interning works under certain conditions - but it's not universal.
- Assuming identical tuples are always the same object: Python may or may not reuse immutable objects depending on how they’re created.
- Modifying the tuple directly: Tuples are immutable - you can’t change individual elements without replacing the whole tuple.
-
Confusing equality with identity: Even though
tuple1 == tuple2
might beTrue
,tuple1 is tuple2
could beFalse
. -
Overusing
id()
without context: Memory references differ with how the object is created - especially when not hardcoded. - Ignoring Python optimizations: The interpreter might optimize some constants; understand when it does and doesn't.
Step-by-Step Thinking
Your goal is to compare two identical-looking tuples returned from a function. First, inspect whether they are the same object in memory, and then test what happens when you alter one of them.
- Create a function that returns a tuple of values
- Call this function twice and assign to
a
andb
- Use
a is b
anda == b
to compare - Print
id(a)
andid(b)
to verify memory location - Reassign one tuple and test
is
again - Log what changes in behavior, if any
How to Make it Harder
Return the tuple from a class method or a dynamically built tuple instead of a literal. Try using integers above 256 and strings longer than 20 characters to observe caching limits.
- Return a tuple created using concatenation:
("dark",) + ("theme",)
- Compare this with a static
("dark", "theme")
- Add large integers or unique strings and see if caching still applies
- Test behavior in loops or in class-based configuration systems
Excercise 2. Singleton Validator
Create a singleton object Sentinel
and use it as a placeholder value in a function default. Inside the function, check whether the passed argument is the same as
the sentinel using the is
operator. This pattern is often used in libraries to distinguish between a missing argument and None
explicitly provided by
the user.
What this Task Teaches
You'll learn how identity checks play a key role in building reliable APIs and libraries. By using sentinel objects and checking with is
, you ensure that the logic
clearly distinguishes between intentional None
and a truly absent value. This concept is frequently used in professional Python projects.
Hints and Tips
Create the sentinel object as SENTINEL = object()
at module level. In your function, check if arg is SENTINEL
to decide how to respond.
- Define
SENTINEL = object()
at the top - Write a function like
def process(value=SENTINEL):
- Inside, check
value is SENTINEL
- Return different outputs for unset, None, and other values
- Test calling the function with no argument, with
None
, and with a string
Common Mistakes
Many developers use None
as the default argument, which can create confusion when None
is a valid user value. A unique sentinel object avoids this
ambiguity. Forgetting to use identity check here can cause logical errors in data processing and function behavior.
-
Using
None
instead of a unique sentinel: If users can passNone
, then you can't distinguish it from default. -
Using
==
instead ofis
: Always use identity comparison for sentinels to avoid equality conflicts. - Not making sentinel global: If you define
object()
inline, it will always be different per call. -
Returning
None
ambiguously: Always clearly document what the return means whenNone
is used. - Forgetting fallback logic: Make sure there's a valid branch when value is the sentinel.
Step-by-Step Thinking
The idea is to distinguish between three cases: no argument passed, None
passed explicitly, and a meaningful value. Using a sentinel object helps with this.
- Define a global sentinel using
SENTINEL = object()
- In your function, use
value=SENTINEL
in the signature - Inside, check
value is SENTINEL
- Handle three cases: no arg,
None
, and other values - Test the function with all three types of calls
How to Make it Harder
Extend this concept by building a config loader that can detect which fields were not set. You can even integrate this with class-level defaults and validators.
- Use multiple sentinels for different fields
- Store unset fields in a dict for later validation
- Raise errors if required fields are not set