Skip to content

Variables and Data Types

A variable in Python is a symbolic name that acts as a reference to an object stored in memory. Unlike some other programming languages, Python does not treat variables as containers that directly hold values; instead, variables point to objects.

Python follows a dynamic typing model, which means that the type of a variable is determined automatically at runtime based on the value assigned to it. There is no need to explicitly declare the data type of a variable.

Key Distinction: Python variables are references to objects, not containers. Understanding this difference is crucial for mastering Python’s memory model.

In Python everything is an object, and objects have three key attributes:

  • Identity → a unique identifier for the object (memory address)
  • Type → the type of the object (e.g., int, str, list)
  • Value → the data stored in the object
x = 10
print(id(x)) # Identity (memory address)
print(type(x)) # Type (int)
print(x) # Value (10)

A variable is created when a value is assigned to it using the assignment operator =.

x = 10
name = "Sahil"
is_active = True

In the above examples:

  • x refers to an integer object
  • name refers to a string object
  • is_active refers to a boolean object
  • A variable is a reference to an object, not the object itself
  • Variables do not store actual data, but rather point to data stored in memory
  • Multiple variables can refer to the same object
  • Variables can be reassigned to objects of different types at runtime

Python uses a reference-based memory model where variables point to objects stored in memory.

x = 10
y = x

Execution flow:

  • An integer object 10 is created in memory
  • x is assigned a reference to that object
  • y is assigned the same reference as x

This means both x and y refer to the same object.

x = 20
  • A new integer object 20 is created
  • x now points to the new object
  • y still points to the old object (10)
graph TD A[x] --> B[10] C[y] --> B

Python provides two different ways to compare objects:

a = [1, 2]
b = [1, 2]
a == b # True → values are equal
a is b # False → different objects in memory
  • == checks value equality
  • is checks object identity (memory reference)

A data type defines the type of value that an object can hold and determines the operations that can be performed on that value.

int # Integer numbers (e.g., 10)
float # Floating-point numbers (e.g., 3.14)
bool # Boolean values (True or False)
str # Strings (text data)
complex # Complex numbers (e.g., 1 + 2j)
NoneType # Represents the absence of a value
list # Ordered, mutable collection
tuple # Ordered, immutable collection
set # Unordered collection of unique elements
frozenset # Unordered, immutable collection of unique elements
dict # Key-value mapping

String is a sequence of characters and has various built-in methods for manipulation:

name = "Sahil"
print(name.upper()) # SAHIL
print(name.lower()) # sahil
print(name.capitalize()) # Sahil
print(name.replace("a", "o")) # Sohil
print(name.split("a")) # ['S', 'hil']
print(name.strip()) # Removes leading and trailing whitespace
print(name.startswith("S")) # True
print(name.endswith("l")) # True
print(name.find("h")) # 2 (index of first occurrence)

String Slicing:

[start:stop:step] → returns a substring based on the specified indices and step.

name = "Sahil"
print(name[0]) # S (first character)
print(name[1:4]) # hil (substring from index 1 to 3)
print(name[:3]) # Sah (first three characters)
print(name[3:]) # il (substring from index 3 to end)
print(name[-1]) # l (last character)
print(name[-3:]) # hil (last three characters)
print(name[::2]) # S a (every second character)

Lists are ordered, mutable collections that have various built-in methods for manipulation:

my_list = [1, 2, 3]
my_list.append(4) # [1, 2, 3, 4]
my_list.insert(1, 10) # [1, 10, 2, 3, 4]
my_list.remove(2) # [1, 10, 3, 4
my_list.pop() # [1, 10, 3] (removes last element)
my_list.pop(1) # [1, 3] (removes element at index 1)
my_list.sort() # Sorts the list in place
my_list.reverse() # Reverses the list in place
my_list.clear() # Empties the list
my_list.extend([5, 6]) # [5, 6] (adds elements from another iterable)
my_list.count(3) # 1 (counts occurrences of 3)
my_list.index(5) # 0 (returns index of first occurrence of 5)
my_list.join(", ") # "5, 6" (joins list elements into a string)

List Slicing:

[start:stop:step] → returns a sublist based on the specified indices and step.

my_list = [1, 2, 3, 4, 5]
print(my_list[0]) # 1 (first element)
print(my_list[1:4]) # [2, 3, 4] (sublist from index 1 to 3)
print(my_list[:3]) # [1, 2, 3] (first three elements)
print(my_list[3:]) # [4, 5] (sublist from index 3 to end)
print(my_list[-1]) # 5 (last element)
print(my_list[-3:]) # [3, 4, 5] (last three elements)
print(my_list[::2]) # [1, 3, 5] (every second element)

Dictionaries are unordered collections of key-value pairs that have various built-in methods for manipulation:

my_dict = {"name": "Sahil", "age": 20}
my_dict["name"] # "Sahil" (access value by key)
my_dict["age"] = 21 # Update value for key "age"
my_dict["city"] = "New York" # Add new key-value pair
del my_dict["age"] # Remove key-value pair with key "age"
my_dict.get("name") # "Sahil" (returns value for key, or None if key not found)
my_dict.keys() # dict_keys(['name', 'city']) (returns a view of keys)
my_dict.values() # dict_values(['Sahil', 'New York']) (returns a view of values)
my_dict.items() # dict_items([('name', 'Sahil'), ('city', 'New York')]) (returns a view of key-value pairs)
my_dict.clear() # Empties the dictionary

Sets are unordered collections of unique elements that have various built-in methods for manipulation:

my_set = {1, 2, 3}
my_set.add(4) # {1, 2, 3, 4} (adds an element to the set)
my_set.remove(2) # {1, 3, 4} (removes an element from the set)
my_set.discard(5) # {1, 3, 4} (does not raise an error if element not found)
my_set.pop() # Removes and returns an arbitrary element from the set
my_set.clear() # Empties the set
my_set.union({5, 6}) # {1, 3, 4, 5, 6} (returns a new set with elements from both sets)
my_set.intersection({3, 4, 5}) # {3, 4} (returns a new set with elements common to both sets)
my_set.difference({3, 4, 5}) # {1} (returns a new set with elements in my_set but not in the other set)
my_set.symmetric_difference({3, 4, 5}) # {1, 5} (returns a new set with elements in either set but not in both)

Frozensets are immutable sets that have various built-in methods for manipulation:

my_frozenset = frozenset([1, 2, 3])
my_frozenset.union(frozenset([4, 5])) # frozenset({1, 2, 3, 4, 5}) (returns a new frozenset with elements from both sets)
my_frozenset.intersection(frozenset([2, 3, 4])) # frozenset({2, 3}) (returns a new frozenset with elements common to both sets)
my_frozenset.difference(frozenset([2, 3, 4])) # frozenset({1}) (returns a new frozenset with elements in my_frozenset but not in the other set)
my_frozenset.symmetric_difference(frozenset([2, 3, 4])) # frozenset({1, 4}) (returns a new frozenset with elements in either set but not in both)

Tuples are ordered, immutable collections that have various built-in methods for manipulation:

my_tuple = (1, 2, 3)
my_tuple[0] # 1 (access element by index)
my_tuple.count(2) # 1 (counts occurrences of 2)
my_tuple.index(3) # 2 (returns index of first occurrence of 3)

Before performing type conversion, it’s important to check the type of the value to ensure that the conversion is valid.

x = "10"
if isinstance(x, str):
print("x is a string")

There are two main way to check types in Python:

  • type() → returns the exact type of the object
  • isinstance() → checks if an object is an instance of a class or a subclass
x = 10
print(type(x)) # <class 'int'>
print(isinstance(x, int)) # True

Type casting refers to the process of converting a value from one data type to another.

In some cases, Python automatically converts one data type to another to prevent data loss.

x = 10 # int
y = 3.5 # float
z = x + y # result is float (10.0 + 3.5 → 13.5)

Explicit conversion is performed using built-in functions.

int("10") # Converts string to integer
float("3.14") # Converts string to float
str(100) # Converts integer to string
bool(1) # Converts integer to boolean
int("abc") # Raises ValueError

Python supports various operations that may involve type compatibility and coercion. For example, when performing arithmetic operations between different types, Python will attempt to convert them to a common type.

x = 10 # int
y = 3.5 # float
result = x + y # result is float (10.0 + 3.5 → 13.5)

Operations between incompatible types will raise a TypeError.

x = "10"
y = 5
result = x + y # Raises TypeError (cannot add str and int)
  • Arithmetic operations (e.g., +, -, *, /)
  • Comparison operations (e.g., ==, !=, <, >)
  • Logical operations (e.g., and, or, not)
  • Membership operations (e.g., in, not in)
  • Identity operations (e.g., is, is not)
  • Bitwise operations (e.g., &, |, ^, ~)
  • Assignment operations (e.g., =, +=, -=, *=, /=)
  • Operations between compatible types are allowed (e.g., int and float)
  • Operations between incompatible types raise a TypeError
  • Python performs implicit type conversion when necessary to prevent data loss

In Python, certain values are considered “truthy” or “falsy” when evaluated in a boolean context (e.g., in an if statement).

  • None
  • False
  • 0 (zero of any numeric type)
  • 0.0 (zero float)
  • 0j (zero complex)
  • '' (empty string)
  • [] (empty list)
  • () (empty tuple)
  • {} (empty dictionary)
  • set() (empty set)
  • Any value that is not falsy is considered truthy, including:
  • Non-empty strings
  • Non-zero numbers
  • Non-empty collections (lists, tuples, sets, dictionaries)
  • Custom objects (instances of user-defined classes)

Objects in Python are categorized based on whether their state can be changed after creation.

An immutable object is an object whose value cannot be modified after it is created. Any operation that appears to modify it actually creates a new object.

x = 10
x = x + 1
  • A new integer object is created
  • The original object remains unchanged

Examples:

  • int
  • float
  • str
  • tuple

lst = [1, 2]
lst.append(3)
  • The same list object is modified
  • Memory reference remains unchanged

Examples:

  • list
  • dict
  • set

FeatureMutable ObjectsImmutable Objects
ModifiableYesNo
Memory usageSame object modifiedNew object created
Exampleslist, dict, setint, str, tuple

Python manages memory using a combination of:

  • Heap memory → stores all objects
  • References (names) → point to objects

Variables are simply names bound to objects.

graph TD A[Variable Name] --> B[Reference] B --> C[Heap Object]

Copying refers to creating a new object based on an existing object. The behavior of copying depends on whether the object is mutable and whether the copy is shallow or deep.


A shallow copy creates a new outer object, but the inner objects are still shared between the original and the copy.

import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
b[0][0] = 100
print(a) # Original is affected

This Diagram illustrates the concept of shallow copy:

graph TD a --> L1[List] b --> L2[New List] L1 --> X L2 --> X
  • List b is a new list object that references the same inner lists as a
  • Modifying the inner list through b also modifies the inner list referenced by a

A deep copy creates a completely independent copy of the object, including all nested objects.

import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0][0] = 100
print(a) # Original is NOT affected

This Diagram illustrates the concept of deep copy:

graph TD a --> L1 b --> L2 L1 --> X1 L2 --> X2
  • List b is a new list object that references new inner lists, so modifications to b do not affect a
  • Deep copying is more memory-intensive than shallow copying, but it ensures complete independence between the original and the copy.

a = [1, 2]
b = a
  • No new object is created
  • Both variables refer to the same object
  • Changes in one affect the other

b = a.copy()
  • A new object is created
  • Only the outer structure is copied (shallow copy)
  • Nested objects may still be shared