Python Basics October 23 ,2025

 

Python Lists —

Python lists are one of the most versatile data structures. Beyond simply storing sequences, lists form the backbone of Python programming, powering algorithms, data manipulation, and even complex applications like machine learning pipelines.

In this guide, we go beyond basic syntax, diving into internal mechanics, performance considerations, and real-world applications.

1. List Internals: How Python Stores Lists

Python lists are dynamic arrays, implemented as arrays of pointers in memory. Key points:

  • Each element is a reference to an object, not the object itself.
  • Lists overallocate memory to optimize append() operations (amortized O(1)).
  • Mutability allows in-place changes, unlike tuples, but copying nested objects requires care.

Memory insight:

import sys
lst = [1, 2, 3]
print(sys.getsizeof(lst))  # Base memory usage
  • Each append() may trigger reallocation if the list exceeds allocated capacity.

2. Advanced Indexing and Slicing

Python lists allow slicing with start:stop:step and support negative indices, but deeper nuances exist:

lst = [10, 20, 30, 40, 50]

# Reverse using slicing
rev = lst[::-1]  # [50, 40, 30, 20, 10]

# Every 2nd element, starting from index 1
subset = lst[1::2]  # [20, 40]
  • Slice assignment can modify multiple elements:
lst[1:4] = [21, 31, 41]  # [10, 21, 31, 41, 50]
  • Be cautious: slicing creates shallow copies, not deep copies.

3. List Methods — With Time Complexity

MethodDescriptionComplexityNotes
append()Add element at endO(1) amortizedEfficient due to over-allocation
extend()Add multiple elementsO(k)k = length of iterable
insert(i, x)Insert at index iO(n)Shifts subsequent elements
pop(i)Remove elementO(n)O(1) if popping last element
remove(x)Remove first occurrenceO(n)Searches linearly
index(x)Return indexO(n)Linear search
sort()In-place sortO(n log n)Timsort algorithm
reverse()Reverse listO(n)In-place operation

4. Shallow vs Deep Copy 

When working with mutable objects like lists, dictionaries, or custom classes in Python, it’s important to understand how copying works.
Simply assigning one variable to another doesn’t actually create a new object — both names refer to the same memory location.
To truly duplicate data, Python provides two types of copying mechanisms: shallow copy and deep copy.

Assignment Is Not a Copy

Before understanding shallow and deep copies, note that assignment (=) does not copy an object — it only creates a new reference to the same object.

a = [1, 2, 3]
b = a
b[0] = 99
print(a)   # [99, 2, 3]

Both a and b point to the same list in memory.
Changing one affects the other because they are aliases, not copies.

The copy Module

To actually create copies, Python’s copy module provides two methods:

  • copy.copy() — performs a shallow copy
  • copy.deepcopy() — performs a deep copy

You must import it before use:

import copy

What Is a Shallow Copy?

A shallow copy creates a new object, but it does not recursively copy the inner objects (nested structures).
Instead, it copies references to the original inner objects.

This means that:

  • The outer object is duplicated.
  • The inner (nested) objects are shared between the original and the copy.

Example:

import copy

list1 = [[1, 2, 3], [4, 5, 6]]
list2 = copy.copy(list1)

list2[0][0] = 99

print("Original:", list1)
print("Shallow Copy:", list2)

Output:

Original: [[99, 2, 3], [4, 5, 6]]
Shallow Copy: [[99, 2, 3], [4, 5, 6]]

Here, changing an inner list in list2 also affects list1, because both share references to the same nested lists.

However, if you change the outer object (like appending or removing elements), it won’t affect the original.

list2.append([7, 8, 9])
print(list1)   # Original list unaffected

What Is a Deep Copy?

A deep copy creates a completely independent clone of the original object — including all nested elements.
That means any changes made to the copy (even deeply nested ones) do not affect the original object.

Example:

import copy

list1 = [[1, 2, 3], [4, 5, 6]]
list2 = copy.deepcopy(list1)

list2[0][0] = 99

print("Original:", list1)
print("Deep Copy:", list2)

Output:

Original: [[1, 2, 3], [4, 5, 6]]
Deep Copy: [[99, 2, 3], [4, 5, 6]]

The two lists are now completely separate — modifying one does not affect the other at any level.

Visual Representation

Let’s visualize it conceptually:

OperationOuter ObjectInner Objects
Assignment (=)SameSame
Shallow CopyDifferentShared
Deep CopyDifferentDifferent

Think of it this way:

  • Assignment → two names, one object
  • Shallow copy → two outer containers, same inner items
  • Deep copy → two containers, each with its own independent contents

When to Use Each

Use shallow copy when:

  • The object is simple (no nested mutable structures).
  • You don’t need to modify nested elements.

Use deep copy when:

  • The object contains nested lists, dicts, or other mutable objects.
  • You want a completely independent clone that can be modified freely.

Performance Considerations

Deep copying can be slower and more memory-intensive because it recursively duplicates every object inside.
Shallow copies are faster and sufficient for most one-level data structures.

For large, complex data structures, it’s wise to assess whether deep copying is truly needed.

Practical Example: Objects and Classes

Shallow vs deep copy also applies to custom objects.

import copy

class Person:
    def __init__(self, name, skills):
        self.name = name
        self.skills = skills

p1 = Person("Alice", ["Python", "ML"])
p2 = copy.copy(p1)       # Shallow copy
p3 = copy.deepcopy(p1)   # Deep copy

p2.skills.append("Data Science")
p3.skills.append("AI")

print(p1.skills)   # ['Python', 'ML', 'Data Science']
print(p2.skills)   # ['Python', 'ML', 'Data Science']
print(p3.skills)   # ['Python', 'ML', 'AI']

Here:

  • p2 shares the same skills list as p1 (shallow copy).
  • p3 has its own separate copy (deep copy).

 

5. Nested Lists —

A nested list is simply a list inside another list.
In Python, lists can hold any type of object — including other lists — which allows you to create multi-dimensional data structures such as matrices, tables, or grids.

This flexibility makes nested lists a powerful tool for representing structured or hierarchical data.

Creating Nested Lists

A nested list is created just like a regular list, but with inner lists as elements.

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

Here, matrix is a list that contains three lists, each representing a row of a 3×3 grid.

  • matrix[0] → [1, 2, 3]
  • matrix[1] → [4, 5, 6]
  • matrix[2] → [7, 8, 9]

This structure can be thought of as:

[
  [1, 2, 3],   # Row 0
  [4, 5, 6],   # Row 1
  [7, 8, 9]    # Row 2
]

Accessing Elements in a Nested List

To access elements, use multiple indices — one for each level of nesting.

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

print(matrix[0])       # First inner list → [1, 2, 3]
print(matrix[0][1])    # Element at row 0, column 1 → 2
print(matrix[2][2])    # Element at row 2, column 2 → 9

You can think of matrix[row][column] as referring to a specific cell in a 2D table.

Modifying Elements

Since lists are mutable, you can modify elements at any depth of nesting.

matrix[1][1] = 99
print(matrix)

Output:

[[1, 2, 3], [4, 99, 6], [7, 8, 9]]

You can also replace entire inner lists:

matrix[0] = [10, 11, 12]
print(matrix)

Output:

[[10, 11, 12], [4, 99, 6], [7, 8, 9]]

Iterating Through Nested Lists

You can use nested loops to access all elements in a nested list.

Example:

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

for row in matrix:
    for value in row:
        print(value, end=" ")
    print()  # Move to the next line after each row

Output:

1 2 3
4 5 6
7 8 9

The outer loop iterates through each inner list, while the inner loop iterates through elements within that list.

Using List Comprehensions

List comprehensions work beautifully with nested lists — especially when flattening or transforming data.

Example 1: Flattening a Nested List

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

flat = [num for row in matrix for num in row]
print(flat)

Output:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

Example 2: Transforming Elements

squared = [[num ** 2 for num in row] for row in matrix]
print(squared)

Output:

[[1, 4, 9], [16, 25, 36], [49, 64, 81]]

Common Use Cases for Nested Lists

  • Matrices and 2D data representation — like spreadsheets or game boards.
  • Adjacency lists — for representing graphs.
  • Hierarchical data — such as JSON-like structures.
  • Grid-based problems — common in algorithms and competitive programming.

Caution: Shallow Copy and Nested Lists

When copying nested lists, using assignment or a shallow copy only duplicates the outer list — the inner lists still share references.
This can lead to unexpected behavior when modifying inner elements.

import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)      # Shallow copy
b[0][0] = 99

print(a)   # [[99, 2], [3, 4]] — also changed!

To avoid this, use a deep copy when working with nested lists.

b = copy.deepcopy(a)
b[0][0] = 100
print(a)   # [[99, 2], [3, 4]] — unchanged
print(b)   # [[100, 2], [3, 4]]

Length and Structure

You can check the number of rows and columns using len().

rows = len(matrix)
cols = len(matrix[0])
print(f"Matrix has {rows} rows and {cols} columns.")

Output:

Matrix has 3 rows and 3 columns.

Nested Lists Are Not True Multidimensional Arrays

While nested lists can simulate multi-dimensional structures, they are not true arrays — meaning all inner lists don’t have to be of equal length.

data = [
    [1, 2, 3],
    [4, 5],
    [6]
]

This is perfectly valid in Python, but it creates a jagged list (lists of unequal lengths).
For true multi-dimensional operations (like matrix multiplication), you’d typically use NumPy arrays.

The Idea Behind Pythonic Iteration

In lower-level languages, iteration often relies on index-based loops — manually incrementing counters and tracking positions.
In contrast, Python promotes high-level, expressive constructs that allow you to loop over the elements themselves rather than over indices.

For example:

fruits = ["apple", "banana", "cherry"]

# Less Pythonic (index-based)
for i in range(len(fruits)):
    print(fruits[i])

# Pythonic
for fruit in fruits:
    print(fruit)

Both produce the same output, but the second version is cleaner, more readable, and less error-prone.
Pythonic iteration focuses on what you want to do, not how to do it.

Iterating Over Different Data Types

Python’s for loop works with any iterable — meaning any object that can return its elements one at a time.
Let’s look at common cases.

Over Lists and Tuples

numbers = [10, 20, 30, 40]
for n in numbers:
    print(n)

Over Strings

A string is iterable too — each iteration gives you one character.

for ch in "Python":
    print(ch)

Over Dictionaries

When you iterate directly over a dictionary, you get its keys by default.

person = {"name": "Alice", "age": 25, "city": "Paris"}

for key in person:
    print(key, "→", person[key])

To get both keys and values at once, use .items():

for key, value in person.items():
    print(f"{key}: {value}")

Over Sets

Sets are unordered collections, but they’re iterable as well:

colors = {"red", "green", "blue"}
for color in colors:
    print(color)

Using enumerate() — Iterating with Index and Value

When you need both the index and the element during iteration, instead of manually tracking a counter, use the built-in enumerate() function.

fruits = ["apple", "banana", "cherry"]

for index, fruit in enumerate(fruits):
    print(index, fruit)

Output:

0 apple
1 banana
2 cherry

enumerate() makes loops cleaner and avoids manual index management. You can even start counting from a custom index:

for index, fruit in enumerate(fruits, start=1):
    print(index, fruit)

Using zip() — Iterating Over Multiple Sequences

When you need to loop through two or more sequences at once, zip() is the most Pythonic way.
It pairs corresponding elements together from each iterable.

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

for name, age in zip(names, ages):
    print(f"{name} is {age} years old")

Output:

Alice is 25 years old
Bob is 30 years old
Charlie is 35 years old

If the lists are of unequal length, iteration stops when the shortest one ends.

List Comprehensions — Compact and Elegant Iteration

List comprehensions are one of the most Pythonic ways to iterate and transform data in a single line.
They replace multi-line loops with a concise, expressive syntax.

Example:

numbers = [1, 2, 3, 4, 5]
squares = [n**2 for n in numbers]
print(squares)

Output:

[1, 4, 9, 16, 25]

You can even add conditions:

even_squares = [n**2 for n in numbers if n % 2 == 0]
print(even_squares)

Output:

[4, 16]

List comprehensions can also be nested for working with nested lists:

matrix = [[1, 2], [3, 4], [5, 6]]
flattened = [num for row in matrix for num in row]
print(flattened)

Dictionary and Set Comprehensions

Python also allows similar shorthand syntax for dictionaries and sets.

Dictionary comprehension:

nums = [1, 2, 3, 4]
squares = {n: n**2 for n in nums}
print(squares)

Output:

{1: 1, 2: 4, 3: 9, 4: 16}

Set comprehension:

unique_squares = {n**2 for n in nums}
print(unique_squares)

Iterating with range()

range() is used to generate sequences of numbers, often for counting-based loops.

for i in range(5):
    print(i)

Output:

0
1
2
3
4

You can specify start, stop, and step:

for i in range(1, 10, 2):
    print(i)

Output:

1
3
5
7
9

Although range() returns a sequence-like object, it doesn’t store all numbers in memory — it’s lazy and efficient, making it suitable for large ranges.

Iterating Over Files and Generators

Python’s file objects and generators are also iterable. This makes it possible to loop over data streams efficiently.

Example — Reading a file line by line:

with open("example.txt") as f:
    for line in f:
        print(line.strip())

This is memory-efficient because only one line is read into memory at a time.

The iter() and next() Functions

Under the hood, all Pythonic iteration uses the iterator protocol — objects that implement __iter__() and __next__().
You can manually access this behavior with the built-in functions iter() and next().

nums = [10, 20, 30]
it = iter(nums)

print(next(it))  # 10
print(next(it))  # 20
print(next(it))  # 30

When there are no more elements, StopIteration is raised.
Normally, the for loop handles this automatically, which is why you rarely need to call next() yourself — but it’s useful for custom iterator logic.

Pythonic Patterns for Iteration

Here are some best practices that make iteration feel natural and Pythonic:

TaskNon-PythonicPythonic
Looping with indexfor i in range(len(data)):for item in data:
Looping with index and valueManual counterfor i, v in enumerate(data):
Looping two listsNested indexingfor x, y in zip(a, b):
Filtering elementsfor + if + append()List comprehension
Flattening listsNested for loopsNested comprehension
Reading filesreadlines() (loads all)for line in f:

 

Real-World Use Cases

  1. Stacks & Queues
stack = []
stack.append(1)
stack.pop()
  1. Data Cleaning
raw = ["apple", None, "banana", "", "cherry"]
clean = [x for x in raw if x]  # Remove empty/null
  1. Matrix/2D Data
rows, cols = 3, 3
grid = [[0]*cols for _ in range(rows)]
  1. Algorithm Implementation
  • Sliding window: lst[i:i+k]
  • Prefix sum arrays
  • Dynamic programming (DP) tables

Performance Tips

  • Prefer append over repeated insert(0, x) — O(n) vs O(1).
  • Use list comprehensions instead of for loops for small transformations.
  • For large nested structures, always use deepcopy carefully.
  • Use generator expressions for memory-efficient iteration.

Conclusion

Python lists are not just “simple arrays” — they are dynamic, flexible, and powerful objects with deep internal mechanics that affect performance. Understanding memory management, shallow vs deep copies, slicing nuances, and iteration strategies will make you a pro Python programmer capable of writing high-performance, bug-free code.

 

Next Blog- Python Sets

 

Sanjiv
0

You must logged in to post comments.

Get In Touch

G06, Kristal Olivine Bellandur near Bangalore Central Mall, Bangalore Karnataka, 560103

+91-8076082435

techiefreak87@gmail.com