Ordered (items keep their order)— insertion order, not sorted order.
nums = [20, 10, 30]
Index-based (you can access items using [index])
first_num = nums[0] # index from 0, 1, ...
last_num = nums[-1] # index in reverse -1, -2, ...
Dynamic/mutable (it can grow or shrink automatically)
nums = [20, 10, 30]
nums.append(80) # adding 80 at the of the list
nums.remove(20) # removing first value of 20
del nums[1] # deleting item at index 1
Can store different data types in one list
nums = [20, 10, 30, "non number"] # ok
Python Array:
import array
a = array.array('i', [10, 20, 30]) # 'i' = integer type
print(a[1]) # Output: 20
Only stores same type, mainly numbers — except it can store array of Unicode characters (single characters) with typecode 'u'
Compare list, array is memory efficient & some level of math
Allows dynamic resize, but is slower (compare to list and NumPy array)
NumPy Array:
import numpy as np
b = np.array([10, 20, 30])
print(b[1]) # Output: 20
print(b + 2) # Output: [12 22 32] (vectorized math)
Only stores same type
Very memory efficient.
Designed for math / scientific computing array
When to Use What:
Use list when: You need convenience; Data types vary; Performance is not critical
Use Python array when: You need compact numeric storage; You are interfacing with C or binary formats; You cannot install NumPy (microcontrollers, Python-on-limited-systems)
Use NumPy ndarray when: You need fast math; Working with matrices, machine learning, scientific computing; Memory and performance matter
for num in nums:
print(num)
enumerate() — Instead of manually tracking indexes:
for index, value in enumerate(["a", "b", "c"]):
print(index, value)
zip() — Loop over two lists at once:
names = ["Alice", "Bob", "Carol"]
ages = [24, 19, 32]
for name, age in zip(names, ages):
print(name, age)
squares = [x**2 for x in range(1, 11)]
This is equivalent to:
squares = []
for x in range(1, 11):
squares.append(x**2)
Example:
nums = [10, 20, 30, 40, 50]
Syntax: list[start : end]
one_three = nums[1:3] # includes index 1 to 2
first_two = nums[:2] # from index 0 to 1
two_end = nums[2:] # from index 2 to last
Note: starting index is inclusive, ending index is exclusive
Full Syntax: list[start : end: step]
nums[::2] # every 2nd element → [10, 30, 50]
nums[::-1] # reverse list → [50, 40, 30, 20, 10]
Copying a list:
copy_of_nums = nums[:]
append(x) — Add x to end
insert(i, x) — Insert x at index i
pop() / pop(i) — Remove and return item (last or index i)
remove(x) — Remove first occurrence of x
sort() — Sort list in place
sorted(list) — Return new sorted list
reverse() — Reverse list in place
index(x) — Return index of x
count(x) — Count occurrences of x
len(list) — Number of items in list
min() — Smallest value
max() — Largest value
sum() — Total of values
import statistics
statistics.mean() — Average
statistics.median() — Middle value
statistics.mode() — Most common/frequent value
statistics.stdev() — Spread/variation
statistics.variance() — Spread squared
immutable_nums = (20, 10, 30)
As tuples are immutable and hashable, they can be used as dictionary keys, lists cannot.
location = (37.77, -122.41)
weather_map[location] = "Sunny"
Unordered: No index-based access (my_set[0] won’t work).
Unique elements: Duplicate values are automatically removed.
Mutable: You can add or remove elements, but the elements themselves must be immutable (e.g., numbers, strings, tuples).
Create a set using curly braces {} or the set() constructor:
# Using curly braces
my_set = {1, 2, 3, 4}
print(my_set) # Output: {1, 2, 3, 4}
# Using set() constructor
my_set2 = set([1, 2, 2, 3])
print(my_set2) # Output: {1, 2, 3} (duplicates removed)
⚠️Important: {} alone creates an empty dictionary, not a set. For an empty set, use set().
s = {1, 2, 3}
# Add elements
s.add(4) # {1, 2, 3, 4}
# Remove elements
s.remove(2) # {1, 3, 4}, raises KeyError if not found
s.discard(5) # {1, 3, 4}, no error if not found
# Check membership
print(3 in s) # True
print(5 in s) # False
Fast membership testing: x in s = O(1)
Uses hashing (direct lookup), as set stores elements in a hash table.
For list & tuple = O(n)
a = {1, 2, 3}
b = {3, 4, 5}
# All elements of a and b
# Equivalent: a | b
print(a.union(b))
# {1, 2, 3, 4, 5}
# Elements common to both a and b.
# Equivalent: a & b
print(a.intersection(b))
# {3}
# Elements in a but NOT in b
# Equivalent: a - b
print(a.difference(b))
# {1, 2}
# Elements in a or b, but not both.
# Equivalent Operator: a ^ b
print(a.symmetric_difference(b))
# {1, 2, 4, 5}
Operators (|, &, -, ^) are shorter and often easier to read.
Methods (union(), intersection(), etc.) are better when:
Passing multiple sets at once (e.g., a.union(b, c, d)).
Working with non-set iterables (lists, tuples), since methods auto-convert:
{1,2}.union([2,3]) # works
{1,2} | [2,3] # ERROR
fs = frozenset([1, 2, 3])
print(fs) # frozenset({1, 2, 3})
As a dictionary key:
mapping = {
frozenset([1, 2, 3]): "group A"
}
To ensure data stays constant or "read-only":
ROLES = frozenset(["admin", "editor", "viewer"])
Keys must be unique, and immutable
✅ Valid keys: str, int, float, tuple, bool
❌ Invalid keys: list, set, dict (because they can change)
Values can be any data type
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Access value by key
age = person["age"]
# Update value
person["age"] = 31
# Add new key–value pair
person["job"] = "Engineer"
# Remove key-value pair
del person["city"]
# If key does not exist, throws KeyError (Unsafe)
height = person.["height"]
Check if key exists before accessing:
if "height" in person:
height = person["height"]
else:
height = "Not specified"
Try–Except Block:
try:
height = person["height"]
except KeyError:
height = "Unknown"
Simple recommended way:
# Returns None (safe)
height = person.get("height")
# With default value (here, default = Unknown"")
height = person.get("height", "Unknown")
General syntax:
{ key_expression : value_expression for item in iterable if condition }
Example:
squares = {x: x*x for x in range(1, 6)}
# With filter items
even_squares = {x: x*x for x in range(10) if x % 2 == 0}
names = ["alice", "bob", "charlie"]
lengths = {name: len(name) for name in names}
# by key-value pairs
for key, value in person.items():
print(key, value)
# by keys
for key in person.keys():
print(key, person.get(key))
# by values
for value in person.values():
print(value)
value = person["name"]
The key ("name") is passed to the hash() function.
The hash value (a number) determines where the value is stored in memory.
Python jumps directly to that position → O(1) lookup time.
"name" ----hash----> index in table → "Alice"
JSON = String format data
Keys types must be string
Values types limited to: string, number, bool, list, object
Converting Between Them
import json
# dict → JSON string
j = json.dumps({'name': 'Alice', 'age': 30})
# JSON string → dict
d = json.loads('{"name": "Alice", "age": 30}')
range(stop) — start = 0 by default
range(start, stop) — start is inclusive, end is exclusive
range(start, stop, step) — step = 1 by default; use negative (-) value to count backwards
# Output: 2, 3, 4, 5, 6 (one by one)
for i in range(2, 7):
print(i)
# Output: 10, 8, 6, 4, 2 (one by one)
for i in range(10, 0, -2):
print(i)
# list: [0, 1, 2, 3, 4]
numbers = list(range(5))
# tuple: (3, 4, 5, 6, 7)
numbers_tuple = tuple(range(3, 8))
Creates tuple-like objects with named fields.
Useful for making code more readable by accessing elements by name instead of index.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
print(p.x) # 10
print(p.y) # 20
Note: The variable name (Point) and the internal class name ('Point') can differ — but usually we keep them the same to avoid confusion.
A double-ended queue, supports adding/removing elements from both ends in O(1) time.
from collections import deque
dq = deque([1, 2, 3])
dq.append(4) # Add to the right
dq.appendleft(0) # Add to the left
dq.pop() # Remove from the right
dq.popleft() # Remove from the left
print(dq) # deque([1, 2, 3])
Faster than list for queue or stack operations.
A dictionary that remembers insertion order.
from collections import OrderedDict
od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3
print(od) # OrderedDict([('a', 1), ('b', 2), ('c', 3)])
In Python 3.7+, regular dict preserves order, but OrderedDict still has extra features, such as:
od.move_to_end(key)
od.popitem(last=False) # pops first item instead of last
If those features not needed → use regular dict.
A dictionary that provides a default value for missing keys.
Avoids KeyError.
from collections import defaultdict
dd = defaultdict(int) # default value is 0
dd['apple'] += 1
dd['banana'] += 2
print(dd) # defaultdict(<class 'int'>, {'apple': 1, 'banana': 2})
Default value for different types:
list: []
set: set()
int: 0
float: 0.0
dict: {}
Combines multiple dictionaries or mappings into a single view.
Useful when dealing with nested scopes or multiple configs.
from collections import ChainMap
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
combined = ChainMap(dict1, dict2)
print(combined['b']) # 2 (first dict has priority)
print(combined['c']) # 4