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:

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 == and is
  • 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.

  1. Assuming copy is identical: Using slicing [:] creates a new list - it’s equal in value, but not the same object.
  2. Using is for value checks: This works only in rare cases, like small integers or cached strings.
  3. Not printing or labeling comparisons: Without clear print messages, it's hard to know what you're testing.
  4. Skipping the id() function: id() helps visualize the identity of each object.
  5. 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 and fruits is same_list
  • Compare fruits == copied_list and fruits 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 if fruits is also updated
  • Modify copied_list and observe if it affects the original
  • Try using copy() method from the copy 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.

  1. Using == None: Python recommends using is for None checks.
  2. Comparing to the wrong type: Comparing empty strings directly to None can cause confusion.
  3. Incorrect assignment: Forgetting to assign None explicitly can make checks fail silently.
  4. Mixing value checks and identity checks: You must decide whether you want to compare content or object.
  5. Overusing is elsewhere: Don’t start using is 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 to int
  • 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() and cfg2 = cfg1 to share a reference
  • Use cfg3 = Config() to create a new instance
  • Compare cfg1 is cfg2 and cfg1 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.

  1. Misinterpreting == as identity: Two objects can have equal attributes but still be different in memory.
  2. Overwriting references: Accidentally assigning one variable over another may break shared logic.
  3. Not printing id(): It's crucial to visualize actual memory references when debugging.
  4. Modifying one instance and expecting another to change: Only shared references will reflect the update.
  5. 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 a theme attribute
  • Assign cfg1 = Config() and then cfg2 = 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 in cfg2 and cfg3
  • 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.

  1. Using == None instead of is None: This is unreliable when custom __eq__ is defined.
  2. Returning nothing: Forgetting to return a value causes None to be printed automatically.
  3. Using mutable default args: Never use [] or {} as default arguments.
  4. Not testing both conditions: Always test with both passed and missing arguments.
  5. 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.

  1. Assuming identical tuples are always the same object: Python may or may not reuse immutable objects depending on how they’re created.
  2. Modifying the tuple directly: Tuples are immutable - you can’t change individual elements without replacing the whole tuple.
  3. Confusing equality with identity: Even though tuple1 == tuple2 might be True, tuple1 is tuple2 could be False.
  4. Overusing id() without context: Memory references differ with how the object is created - especially when not hardcoded.
  5. 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 and b
  • Use a is b and a == b to compare
  • Print id(a) and id(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.

  1. Using None instead of a unique sentinel: If users can pass None, then you can't distinguish it from default.
  2. Using == instead of is: Always use identity comparison for sentinels to avoid equality conflicts.
  3. Not making sentinel global: If you define object() inline, it will always be different per call.
  4. Returning None ambiguously: Always clearly document what the return means when None is used.
  5. 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

Author of This Exercises

Jason Miller

Jason Miller

I'm a seasoned Python backend developer with over six years of experience working...

Learn more →