paint-brush
A Fresh Perspective on 'is' and '==' Operators in Pythonby@ifoysol
418 reads
418 reads

A Fresh Perspective on 'is' and '==' Operators in Python

by Ishtiaque FoysolApril 18th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this short article we will use `is` keyword and `==` operator to understand the `Pythonic` concepts of assignment, shallow and deep copy. We will also look at how to understand different types of copies in Python.
featured image - A Fresh Perspective on 'is' and '==' Operators in Python
Ishtiaque Foysol HackerNoon profile picture


Currently, I am using Python and JavaScript simultaneously to automate tests for two projects using Selenium and Postman.


I am relatively new to JavaScript and had a tight timeline to learn it. So, I challenged myself to learn its “vanilla basics” in just seven days. I noticed that the features of its object type are quite similar to Python's dictionary data structure, including the assignment, shallow copy, and deep copy features which I will share in my next articles.


In this short article, we will use the is keyword and == operator to understand the Pythonic concepts of assignment, shallow and deep copy.


How do ‘Is’ and ‘==’ differ?

The is keyword checks if two variables point to the same object while == checks if two variables have the same value or if they are equal to the same value.


This sounds simple but can get complex in practical scenarios:

>>> a = 256
>>> b = 256
>>> a is b
True
>>> id(a), id(b) # because they point to the same memory location 
(140501052082576, 140501052082576)
>>> a == b
True


In the above example, variables a and b point to the same memory location, 140501052082576, in my computer RAM after I assigned a and b with the same value of 256. In other words, they are the same object with two different variable names.


So, both the statements a is b and a == b are True as expected.

But things get a bit weird if we change the value to 257.


>>> a = 257
>>> b = 257
>>> a == b
True
>>> a is b # The twist is here
False 


The twist is that a and b now point to two different RAM locations. They are no more the same object!


The Proof of Concept:

>>> id(a), id(b)
(140501049663216, 140501049662992)


Python Documentation says


The current implementation keeps an array of integer objects for all integers between -5 and 256. When you create an int in that range you actually just get back a reference to the existing object.


Now we are equipped with two important pieces of information:


  • is compares if two variables refer to the same object. In other words, if both the variables are the same object in a memory location


  • == compares if two variables have the same value

Let’s utilize our knowledge to understand different types of copies in Python.


Assignment, Shallow and Deep Copy

Assignment

First, we will declare and populate a dict_1 with some data and assign it to dict_2. Now, dict_1 and dict_1 will point to the same object.

# Example 1
dict_1 = {
	'guitar': 'Yamaha',
	'strings': 7, 
	
	'pedals':{
		'cry_baby': 'Electro-Harmonix',
		'distortion': 'Boss', 
		'delay': 'TC Electronics'
	},
	
	'numbers': [1,2,3], 
}

dict_2 = dict_1

print(id(dict_1), id(dict_2))
print(dict_1 is dict_2)

"""
$ python3 example.py
140283083210048 140283083210048
True
"""


Any key:value changes in either of the variable, any type or nested, will bring change on the reference object that will be reflected in both variables.


So, both their values and reference objects remain the same.

# Example 2
dict_2['delay'] = 'Line 6'

print(id(dict_1), id(dict_2))
print(dict_1 == dict_2)
print(dict_1 is dict_2) 

"""
$ python3 examples.py 
140319473249600 140319473249600
True
True
"""


Shallow Copy

Python 3x ’s builtin copy module lets us do shallow and deep copies. Let’s start with a shallow copy.


shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.


In other words, a shallow copy creates a new object that holds a reference to the objects found in the parent or original variable.


So, they are different objects.

# Example 3
import copy

dict_1 = {
	'guitar': 'Yamaha',
	'strings': 7, 
	
	'pedals':{
		'cry_baby': 'Electro-Harmonix',
		'distortion': 'Boss', 
		'delay': 'TC Electronics'
	},
	
	'numbers': [1,2,3], 
}

dict_2 = copy.copy(dict_1)  

print(id(dict_1), id(dict_2))
print(id(dict_1['guitar']), id(dict_2['guitar']))
print(id(dict_1['pedals']), id(dict_2['pedals']))

"""
$ python hello.py
1448723023744 1448723087936 # Different objects created 
1448723023792 1448723023792 # Points to same guitar
1448723023488 1448723023488 # Points to same pedals
"""


Notice two things:

  • dict_1 and dict_2 have two different memory addresses. Here, dict_2 holds reference to the objects found in dict_1


  • dict_2 holds reference to guitar and pedal objects in dict_1


statement 1 If we bring any change in the immutable or non-nested values in dict_1 that will not affect dict_2 and vice-versa.


statement 2 If we bring any change in the mutable or nested values in dict_1 that will affect dict_2 and vice-versa.


Shallow Copy: Changes on nested or mutable data

Let’s start with statement 2. Let’s change the distortion value of dict_2 into Zoom.


Take a deep breath and some time to understand what just happened:

# Example 4
dict_2['pedals']['distortion'] = "Zoom"
print(dict_1 is dict_2)
print(dict_2 == dict_1)

"""
$ python3 example.py 
False
True
"""


💡 Explanation The change made in the nested dictionary in dict_2 were brought in two different objects, dict_1 and dict_1 situated at 1448723023744 and 1448723087936 in my computer RAM. So, dict_1 is dict_2 returns False. But both the objects have the same value and dict_2 == dict_1 the statement returns True.


Why does this happen?


Because, when we bring any change in pedals this refers to the same memory location in the RAM.


Have a look at the PoC

print(f"Before Change id(dict_2['pedals']): {id(dict_2['pedals'])}\
	  id(dict_1['pedals']): {id(dict_1['pedals'])}")

dict_2['pedals']['distortion'] = "Zoom"

print(f"After Change id(dict_2['pedals']): {id(dict_2['pedals'])}\
	  id(dict_1['pedals']): {id(dict_1['pedals'])}")

"""
$ python3 example.py 
Before Change id(dict_2['pedals']): 140285477350656 id(dict_1['pedals']): 140285477350656
After Change id(dict_2['pedals']): 140285477350656  id(dict_1['pedals']): 140285477350656
"""


Task

Modify the numbers in dict_1 and see what happens to dict_2


Shallow Copy: Changes on immutable data

Let’s get back to statement 1 that says “If we bring any change in the immutable or non-nested values in dict_1 that will not affect dict_2 and vice-versa.”

Bring a change in the guitar key’s value in dict_1 and see the effect

dict_1['guitar'] = 'Ibanez'
print(dict_1 is dict_2)
print(dict_2 == dict_1)

"""
$ python3 example.py 
False                                                                                          
False
""" 


💡 Explanation The change brought in dict_1's guitar value takes place in two different objects as well. But, dict_1's guitar now points to a different memory location that left dict_2's guitar untouched.

So, neither dict_1 and dict_2 are same objects nor they have same values.


Have a look at the PoC

print("Before assignment")
print(dict_1 is dict_2)
print(id(dict_1['guitar']), id(dict_2['guitar']) )
print(dict_1['guitar'] is dict_2['guitar'])

dict_1['guitar'] = 'Ibanez'

print("After assignment")
print(dict_1 is dict_2)
print(id(dict_1['guitar']), id(dict_2['guitar']) )
print(dict_1['guitar'] is dict_2['guitar'])

"""
$ python3 example.py 
Before assignment
False
139863551636464 139863551636464
True 
After assignment                                                                                             
False
139863550623600 139863551636464
False
"""


Deep Copy

If a reader came here reading the whole text along with executing the examples, this segment needs only a few words and examples

Python Documentation says


deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.


In other words, a deep copy creates a new object that recursively creates a separate copy of the objects from the parent / original variable. A deep copy ensures that any changes in the original or assigned variable will not affect each other.


import copy

dict_1 = {
	'guitar': 'Yamaha',
	'strings': 7, 
	
	'pedals':{
		'cry_baby': 'Electro-Harmonix',
		'distortion': 'Boss', 
		'delay': 'TC Electronics'
	},
	
	'numbers': [1,2,3], 
}

dict_2 = copy.deepcopy(dict_1)  
print(id(dict_2), id(dict_1)) # ...1
print(id(dict_2['strings']), id(dict_1['strings'])) # ...2
print(id(dict_2['pedals']), id(dict_1['pedals'])) # ...3

"""
$ python3 Shallow.py 
140331302249472 140331302661568
140331303287280 140331303287280                    
140331300866368 140331302661376
"""


💡 Explanation

Notice that a deepcopy() of dict_1


  1. Assigns a separate memory location for dict_2
  2. Any changes in strings key will point to a new object/memory location just like a shallow copy
  3. Assigns different memory location for dict_2 mutable and nested data. This ensures that any changes in dict_1 or dict_2 will affect each other.


Wrapping it up

Almost every available write-up on deep and shallow copy in Python use nested list examples. I found this data type leaves a noob to intermediate programmer a bit confused, at least they need some extra effort to get the concept.


While learning copying JS objects, I found dictionary data structure would help them to visualize the Pythonic concept of deep and shallow copy concepts a bit more clear.