Skip to content
πŸ’» 🧠 Code 1001 > πŸ“š Learning Materials > 🐍 Python > πŸ“˜ Collections in Python

πŸ“˜ Collections in Python

Collections typically support:

  • iteration (for item in collection)
  • membership testing (x in collection)
  • length determination (len(collection))
  • indexed or keyed access (if ordered or associative)

Python has no strict β€œcollection interface”, but follows informal protocols. If an object supports __iter__, __len__, __contains__, it can be considered a collection.


What is NOT a collection

The following types are not collections, as they do not contain groups of elements:

  • int, float, bool β€” scalar values
  • None β€” absence of value
  • functions, modules, classes β€” these are objects, but not data containers (unless they contain __dict__)

Built-in collections

Available without imports:

TypeDescription
listOrdered, mutable sequence.
tupleOrdered, immutable sequence.
dictOrdered (since Python 3.7) key-value mapping.
setUnordered collection of unique elements.
frozensetImmutable version of set.

Extended collections from the standard library

TypeModulePurpose
SimpleNamespacetypesObject with dynamic attributes (dot-access alternative to dict).
namedtuplecollectionsImmutable tuple with named fields.
dequecollectionsDouble-ended queue β€” efficient appends/pops from both ends.
CountercollectionsDictionary subclass for counting hashable objects.
defaultdictcollectionsDictionary with default values for missing keys.
dataclassdataclassesAuto-generates __init__, __repr__, __eq__, etc.
UserListcollectionsBase class for custom list-like objects.
UserDictcollectionsBase class for custom dict-like objects.

Other collection-like types

Though not always called β€œcollections”, these types also represent or store groups of data.

1. str β€” string

Immutable ordered sequence of characters.

s = "Python"
print(len(s))        # β†’ 6
print(s[0])          # β†’ P
print('y' in s)      # β†’ True
print(list(s))       # β†’ ['P', 'y', 't', 'h', 'o', 'n']

2. bytes, bytearray

  • bytes β€” immutable sequence of bytes.
  • bytearray β€” mutable version.
b = b"hello"
print(b[0])          # β†’ 104
print(len(b))        # β†’ 5

ba = bytearray(b"hello")
ba[0] = 72
print(ba)            # β†’ bytearray(b'Hello')

3. range

Lazy ordered numeric sequence. Does not store elements in memory.

r = range(3)
print(list(r))       # β†’ [0, 1, 2]
print(1 in r)        # β†’ True
print(r[2])          # β†’ 2

4. array.array

Stores homogeneous numeric data compactly (like C arrays).

from array import array
arr = array('i', [1, 2, 3])  # 'i' = signed int
print(arr)                   # β†’ array('i', [1, 2, 3])

5. Generators and iterators

Do not store data β€” generate on demand. Do not support len() or indexing.

gen = (x * 2 for x in range(3))
print(list(gen))     # β†’ [0, 2, 4]
# len(gen) β†’ TypeError

6. ChainMap (from collections)

Groups multiple dictionaries into a single view β€” lookup searches through maps in order.

from collections import ChainMap

d1 = {'a': 1}
d2 = {'b': 2}
cm = ChainMap(d1, d2)
print(cm['a'])       # β†’ 1
print(cm['b'])       # β†’ 2

7. OrderedDict (from collections)

Dictionary that remembers insertion order. Relevant for Python < 3.7.

from collections import OrderedDict

od = OrderedDict([('a', 1), ('b', 2)])
print(od)            # β†’ OrderedDict([('a', 1), ('b', 2)])

8. enum.Enum, enum.Flag

Collections of named constants.

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2

print(list(Color))   # β†’ [<Color.RED: 1>, <Color.GREEN: 2>]

9. typing.NamedTuple, typing.TypedDict

Type-annotated wrappers over namedtuple and dict.

from typing import NamedTuple, TypedDict

class Person(NamedTuple):
    name: str
    age: int

p = Person("Alice", 25)

class Movie(TypedDict):
    title: str
    year: int

m: Movie = {"title": "Matrix", "year": 1999}

10. heapq, bisect β€” tools, not collections

Work with collections but are not collections themselves:

  • heapq β€” heap queue algorithm (priority queues via lists).
  • bisect β€” maintain sorted order in lists.

1. Lists β€” list

Ordered, mutable collection. Elements can repeat, any types allowed.

Used when you need a flexible sequence: adding, removing, modifying elements.

Creation: []

boris_list = ["Boris", "Moscow", 30, "engineer"]
print(f"List creation: {boris_list}")
# β†’ List creation: ['Boris', 'Moscow', 30, 'engineer']

print(f"Element at index 0: {boris_list[0]}")
# β†’ Element at index 0: Boris

boris_list[2] = 31
print(f"After modification: {boris_list}")
# β†’ After modification: ['Boris', 'Moscow', 31, 'engineer']

boris_list.append("married")
print(f"After append: {boris_list}")
# β†’ After append: ['Boris', 'Moscow', 31, 'engineer', 'married']

boris_list.insert(1, "Russia")
print(f"After insert: {boris_list}")
# β†’ After insert: ['Boris', 'Russia', 'Moscow', 31, 'engineer', 'married']

boris_list.remove("engineer")
print(f"After remove by value: {boris_list}")
# β†’ After remove by value: ['Boris', 'Russia', 'Moscow', 31, 'married']

del boris_list[2]
print(f"After delete by index: {boris_list}")
# β†’ After delete by index: ['Boris', 'Russia', 31, 'married']

boris_list.extend(["hobby", "fishing"])
print(f"After extend: {boris_list}")
# β†’ After extend: ['Boris', 'Russia', 31, 'married', 'hobby', 'fishing']

boris_list.pop()
print(f"After pop: {boris_list}")
# β†’ After pop: ['Boris', 'Russia', 31, 'married', 'hobby']

2. Dictionaries β€” dict

Collection of key β†’ value pairs. Keys must be hashable. Since Python 3.7, insertion order is preserved.

Useful for structured data: profiles, configs, JSON.

Creation: {}

alice_dict = {"name": "Alice", "age": 25, "city": "London", "occupation": "artist"}
print(f"Dict creation: {alice_dict}")
# β†’ Dict creation: {'name': 'Alice', 'age': 25, 'city': 'London', 'occupation': 'artist'}

print(f"Value by key 'name': {alice_dict['name']}")
# β†’ Value by key 'name': Alice

alice_dict["age"] = 26
print(f"After update: {alice_dict}")
# β†’ After update: {'name': 'Alice', 'age': 26, 'city': 'London', 'occupation': 'artist'}

alice_dict["hobby"] = "painting"
print(f"After adding pair: {alice_dict}")
# β†’ After adding pair: {'name': 'Alice', 'age': 26, 'city': 'London', 'occupation': 'artist', 'hobby': 'painting'}

del alice_dict["city"]
print(f"After deleting pair: {alice_dict}")
# β†’ After deleting pair: {'name': 'Alice', 'age': 26, 'occupation': 'artist', 'hobby': 'painting'}

hobby = alice_dict.pop("hobby")
print(f"After pop: {alice_dict}, value: {hobby}")
# β†’ After pop: {'name': 'Alice', 'age': 26, 'occupation': 'artist'}, value: painting

print(f"Key 'name' exists: {'name' in alice_dict}")
# β†’ Key 'name' exists: True

3. Tuples β€” tuple

Ordered, immutable collection. Suitable for fixed data.

Used when immutability matters: coordinates, parameters, return values.

Creation: ()

boris_tuple = ("Boris", "Moscow", 30, "engineer")
print(f"Tuple creation: {boris_tuple}")
# β†’ Tuple creation: ('Boris', 'Moscow', 30, 'engineer')

print(f"Element at index 2: {boris_tuple[2]}")
# β†’ Element at index 2: 30

# boris_tuple[0] = "Ivan"  β†’ TypeError
# boris_tuple.append("something") β†’ AttributeError
  • Tuples use less memory and are faster than lists.
  • Ideal when mutability is not needed.

4. SimpleNamespace

Simple class from types for creating objects with dynamic attributes. Access via dot notation (obj.attr).

Useful when you want obj.name syntax without defining a full class.

from types import SimpleNamespace

alice_ns = SimpleNamespace(name="Alice", age=25, city="London")
print(f"Object: {alice_ns}")
# β†’ Object: namespace(name='Alice', age=25, city='London')

print(f"Name: {alice_ns.name}")
# β†’ Name: Alice

alice_ns.age = 26
print(f"After update: {alice_ns}")
# β†’ After update: namespace(name='Alice', age=26, city='London')

alice_ns.occupation = "artist"
print(f"With new attribute: {alice_ns}")
# β†’ With new attribute: namespace(name='Alice', age=26, city='London', occupation='artist')

del alice_ns.city
print(f"After deletion: {alice_ns}")
# β†’ After deletion: namespace(name='Alice', age=26, occupation='artist')

setattr(alice_ns, "hobby", "painting")
print(f"Via setattr: {alice_ns}")
# β†’ Via setattr: namespace(name='Alice', age=26, occupation='artist', hobby='painting')

delattr(alice_ns, "hobby")
print(f"Via delattr: {alice_ns}")
# β†’ Via delattr: namespace(name='Alice', age=26, occupation='artist')
  • Alternative to dict when obj.name is preferable to obj['name'].

5. Sets β€” set

Unordered collection of unique elements. Supports set operations: union, intersection, difference.

Used for deduplication and membership testing.

Creation: {} or set()

numbers = {1, 2, 3, 3, 2, 1}
print(f"Set: {numbers}")
# β†’ Set: {1, 2, 3}

numbers.add(4)
print(f"After add: {numbers}")
# β†’ After add: {1, 2, 3, 4}

numbers.remove(2)
print(f"After remove: {numbers}")
# β†’ After remove: {1, 3, 4}

other = {3, 4, 5}
print(f"Union: {numbers | other}")
# β†’ Union: {1, 3, 4, 5}

print(f"Intersection: {numbers & other}")
# β†’ Intersection: {3, 4}

print(f"Difference: {numbers - other}")
# β†’ Difference: {1}

6. Immutable sets β€” frozenset

Immutable version of set. Can be used as dict keys or set elements.

frozen = frozenset([1, 2, 3, 2])
print(f"frozenset: {frozen}")
# β†’ frozenset: frozenset({1, 2, 3})

other = frozenset([3, 4])
print(f"Intersection: {frozen & other}")
# β†’ Intersection: frozenset({3})

print(f"Union: {frozen | other}")
# β†’ Union: frozenset({1, 2, 3, 4})

# frozen.add(5) β†’ AttributeError

7. namedtuple β€” named tuples

Immutable structure with named field access. More readable than plain tuples.

from collections import namedtuple

Person = namedtuple("Person", ["name", "age", "city"])
alice = Person("Alice", 25, "London")

print(f"Object: {alice}")
# β†’ Object: Person(name='Alice', age=25, city='London')

print(f"Name: {alice.name}")
# β†’ Name: Alice

print(f"Age: {alice[1]}")
# β†’ Age: 25

# alice.age = 26 β†’ AttributeError

alice_new = alice._replace(age=26)
print(f"Copy with update: {alice_new}")
# β†’ Copy with update: Person(name='Alice', age=26, city='London')
  • Ideal for records: points, users, configs β€” when immutability and readability matter.

8. deque β€” double-ended queue

Optimized for fast appends/pops at both ends. More efficient than list for appendleft, popleft.

from collections import deque

d = deque([1, 2, 3])
print(f"Initial deque: {d}")
# β†’ Initial deque: deque([1, 2, 3])

d.appendleft(0)
print(f"After appendleft: {d}")
# β†’ After appendleft: deque([0, 1, 2, 3])

d.append(4)
print(f"After append: {d}")
# β†’ After append: deque([0, 1, 2, 3, 4])

left = d.popleft()
print(f"After popleft: {left}, remaining: {d}")
# β†’ After popleft: 0, remaining: deque([1, 2, 3, 4])

right = d.pop()
print(f"After pop: {right}, remaining: {d}")
# β†’ After pop: 4, remaining: deque([1, 2, 3])
  • Used in algorithms: BFS, LRU caches, buffers β€” where end operations must be fast.

9. Counter β€” element counter

Counts frequency of elements in an iterable. Useful for statistics and analysis.

from collections import Counter

text = "abracadabra"
c = Counter(text)
print(f"Letter counts: {c}")
# β†’ Letter counts: Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

print(f"Frequency of 'a': {c['a']}")
# β†’ Frequency of 'a': 5

print(f"Top 3: {c.most_common(3)}")
# β†’ Top 3: [('a', 5), ('b', 2), ('r', 2)]

c2 = Counter("bukva")
c.update(c2)
print(f"After update: {c}")
# β†’ After update: Counter({'a': 6, 'b': 3, 'r': 2, 'c': 1, 'd': 1, 'u': 1, 'k': 1, 'v': 1})
  • Useful for text analysis, logs, voting β€” anywhere you need to count β€œwhat appears most often”.

10. defaultdict β€” dictionary with defaults

Automatically creates default values for missing keys. Eliminates if key in dict checks.

from collections import defaultdict

dd_list = defaultdict(list)
dd_list["fruits"].append("apple")
dd_list["fruits"].append("banana")
print(f"List: {dict(dd_list)}")
# β†’ List: {'fruits': ['apple', 'banana']}

dd_int = defaultdict(int)
for char in "abracadabra":
    dd_int[char] += 1
print(f"Counts: {dict(dd_int)}")
# β†’ Counts: {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}

dd_set = defaultdict(set)
dd_set["cities"].add("Moscow")
dd_set["cities"].add("SPb")
print(f"Set: {dict(dd_set)}")
# β†’ Set: {'cities': {'Moscow', 'SPb'}}
  • Eliminates boilerplate like if key not in d: d[key] = [].
  • Makes code cleaner and safer.

11. dataclass β€” data classes

Decorator that auto-generates __init__, __repr__, __eq__, etc.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
    city: str = "Not specified"

alice = Person("Alice", 25)
print(f"Object: {alice}")
# β†’ Object: Person(name='Alice', age=25, city='Not specified')

print(f"Name: {alice.name}")
# β†’ Name: Alice

alice.age = 26
print(f"After update: {alice}")
# β†’ After update: Person(name='Alice', age=26, city='Not specified')

bob = Person("Bob", 30)
print(f"Alice == Bob: {alice == bob}")
# β†’ Alice == Bob: False

@dataclass(frozen=True)
class ImmutablePerson:
    name: str
    age: int

ivan = ImmutablePerson("Ivan", 40)
# ivan.age = 41 β†’ FrozenInstanceError
  • Replaces manual __init__, __repr__, __eq__.
  • Ideal for DTOs, configs, models.

12. UserList β€” custom lists

Inherits from collections.UserList. Used to create lists with custom behavior.

from collections import UserList

class LoggingList(UserList):
    def append(self, item):
        print(f"[LOG] Append: {item}")
        super().append(item)

    def remove(self, item):
        print(f"[LOG] Remove: {item}")
        super().remove(item)

log_list = LoggingList([1, 2, 3])
print(f"Initial: {log_list}")
# β†’ Initial: [1, 2, 3]

log_list.append(4)
# β†’ [LOG] Append: 4
print(f"After append: {log_list}")
# β†’ After append: [1, 2, 3, 4]

log_list.remove(2)
# β†’ [LOG] Remove: 2
print(f"After remove: {log_list}")
# β†’ After remove: [1, 3, 4]
  • Useful for adding logging, validation, or modifying standard list behavior.

13. UserDict β€” custom dictionaries

Inherits from collections.UserDict. Used to create dicts with custom behavior.

from collections import UserDict

class LowerKeyDict(UserDict):
    def __setitem__(self, key, value):
        key = key.lower() if isinstance(key, str) else key
        super().__setitem__(key, value)

    def __getitem__(self, key):
        key = key.lower() if isinstance(key, str) else key
        return super().__getitem__(key)

ld = LowerKeyDict()
ld["Name"] = "Alice"
print(f"Value by 'Name': {ld['Name']}")
# β†’ Value by 'Name': Alice
print(f"Value by 'name': {ld['name']}")
# β†’ Value by 'name': Alice
print(f"Keys: {list(ld.keys())}")
# β†’ Keys: ['name']
  • Used for key normalization, validation, logging, caching, etc.

πŸ“ˆ Memory and Performance Comparison

Collection choice impacts performance and memory usage. Below are practical benchmarks for common scenarios.


1. Memory: list vs tuple vs array.array

import sys
from array import array

n = 1_000_000
data = list(range(n))
data_t = tuple(range(n))
data_a = array('i', range(n))

print(f"list:  {sys.getsizeof(data) / 1024 / 1024:.2f} MB")
# β†’ list:  8.00 MB

print(f"tuple: {sys.getsizeof(data_t) / 1024 / 1024:.2f} MB")
# β†’ tuple: 8.00 MB

print(f"array: {sys.getsizeof(data_a) / 1024 / 1024:.2f} MB")
# β†’ array: 3.81 MB
  • array.array uses ~2x less memory for numbers.
  • list and tuple consume similar memory, but tuple is slightly faster for iteration.

2. Access speed: list vs tuple vs array.array

import time

def time_access(collection, name):
    start = time.perf_counter()
    total = 0
    for i in range(len(collection)):
        total += collection[i]
    end = time.perf_counter()
    print(f"{name}: {end - start:.4f} seconds")

n = 10_000_000
lst = list(range(n))
tpl = tuple(range(n))
arr = array('i', range(n))

time_access(lst, "list")   # β†’ list: 1.2000 seconds
time_access(tpl, "tuple")  # β†’ tuple: 1.0000 seconds
time_access(arr, "array")  # β†’ array: 0.8000 seconds
  • array.array is fastest for numeric data.
  • tuple is 10–20% faster than list.
  • Difference is noticeable at scale.

3. Memory: dict vs SimpleNamespace vs dataclass

d = {"name": "A", "age": 25, "city": "X", "hobby": "Y", "job": "Z"}
ns = SimpleNamespace(name="A", age=25, city="X", hobby="Y", job="Z")
dc = PersonDC("A", 25, "X", "Y", "Z")

print(f"dict:      {sys.getsizeof(d)} bytes")          # β†’ 232
print(f"SimpleNamespace: {sys.getsizeof(ns)} bytes")   # β†’ 64
print(f"dataclass: {sys.getsizeof(dc)} bytes")         # β†’ 64
print(f"ns.__dict__: {sys.getsizeof(ns.__dict__)} bytes")  # β†’ 232
  • SimpleNamespace and dataclass use same memory as dict due to __dict__.
  • Use __slots__ for memory savings.

4. Memory optimization: dataclass with __slots__

@dataclass
class PersonSlots:
    __slots__ = ("name", "age", "city", "hobby", "job")
    name: str
    age: int
    city: str
    hobby: str
    job: str

dc_slots = PersonSlots("A", 25, "X", "Y", "Z")
print(f"dataclass + slots: {sys.getsizeof(dc_slots)} bytes")
# β†’ 80 bytes

# dc_slots.new = "value" β†’ AttributeError
  • __slots__ saves memory and speeds up attribute access.
  • Trade-off: no dynamic attribute addition.

5. Lookup speed: list vs set

n = 1_000_000
lst = list(range(n))
st = set(range(n))

def time_in(collection, target, name):
    start = time.perf_counter()
    for _ in range(1000):
        _ = target in collection
    end = time.perf_counter()
    print(f"{name} (search {target}): {end - start:.4f} seconds")

time_in(lst, 999_999, "list")   # β†’ 10.0000 seconds
time_in(st, 999_999, "set")     # β†’ 0.0005 seconds
  • set is thousands of times faster than list for membership testing.
  • Always use set for frequent x in collection checks.

6. Memory: set vs frozenset

s = set(range(1000))
fs = frozenset(range(1000))

print(f"set:       {sys.getsizeof(s)} bytes")     # β†’ 32792
print(f"frozenset: {sys.getsizeof(fs)} bytes")   # β†’ 32792
  • frozenset and set use identical memory.
  • Difference is only mutability.

7. Append speed: list.append vs deque.append vs deque.appendleft

from collections import deque
import time

def time_append(collection, n, method='append'):
    start = time.perf_counter()
    for i in range(n):
        if method == 'appendleft' and hasattr(collection, 'appendleft'):
            collection.appendleft(i)
        else:
            collection.append(i)
    end = time.perf_counter()
    return end - start

n = 100_000

lst = []
dq = deque()

time_list_append = time_append(lst, n)              # β†’ 0.0100 seconds
time_deque_append = time_append(dq, n)              # β†’ 0.0100 seconds
time_deque_appendleft = time_append(deque(), n, 'appendleft')  # β†’ 0.0100 seconds

# list.insert(0):
lst = []
start = time.perf_counter()
for i in range(n):
    lst.insert(0, i)
end = time.perf_counter()
print(f"list.insert(0):   {end - start:.4f} seconds")   # β†’ 5.0000 seconds
  • deque.appendleft is O(1), unlike list.insert(0) which is O(n).
  • Use deque for frequent operations at both ends.

🧠 Performance Recommendations

SituationUseReason
Storing numbers, memory criticalarray.array2x less memory, faster access
Data is immutabletupleFaster than list, safer
Frequent x in collection checksset / frozensetO(1) vs O(n) for list
Operations at both endsdequeappendleft/popleft in O(1)
Structured data, memory criticaldataclass + __slots__No __dict__, less memory
Counting frequenciesCounterOptimized for this task
Custom behaviorUserList / UserDictSafe extension of built-ins

πŸ“Š Collection Comparison

TypeOrderedMutableUnique ElementsIndex AccessDuplicates
listβœ… Yesβœ… Yes❌ Noβœ… Yesβœ… Yes
tupleβœ… Yes❌ No❌ Noβœ… Yesβœ… Yes
dictβœ… Yes*βœ… YesKeys only❌ NoValues: βœ…
set❌ Noβœ… Yesβœ… Yes❌ No❌ No
frozenset❌ No❌ Noβœ… Yes❌ No❌ No
SimpleNamespaceβœ… Yes (attrs)βœ… Yes❌ No (attrs can repeat semantically)❌ Noβœ… Yes
namedtupleβœ… Yes❌ No❌ Noβœ… Yesβœ… Yes
dequeβœ… Yesβœ… Yes❌ Noβœ… Yesβœ… Yes
Counter❌ Noβœ… Yes❌ No❌ No (but has keys)βœ… Yes
defaultdictβœ… Yes*βœ… YesKeys only❌ NoValues: βœ…
dataclassβœ… Yes (fields)βœ… Yes (if not frozen)❌ No❌ Noβœ… Yes
UserListβœ… Yesβœ… Yes❌ Noβœ… Yesβœ… Yes
UserDictβœ… Yes*βœ… YesKeys only❌ NoValues: βœ…
strβœ… Yes❌ No❌ Noβœ… Yesβœ… Yes
bytesβœ… Yes❌ No❌ Noβœ… Yesβœ… Yes
bytearrayβœ… Yesβœ… Yes❌ Noβœ… Yesβœ… Yes
rangeβœ… Yes❌ No❌ Noβœ… Yes❌ No
array.arrayβœ… Yesβœ… Yes❌ Noβœ… Yesβœ… Yes
ChainMapβœ… Yes*βœ… YesKeys only❌ NoValues: βœ…
Enumβœ… Yes❌ Noβœ… Yes (members)❌ No❌ No
  • β€” since Python 3.7, dict, defaultdict, UserDict, ChainMap preserve insertion order.

πŸ’‘ When to Use What?

TaskCollection
Mutable sequencelist / deque / UserList / bytearray
Immutable datatuple / namedtuple / frozenset / str / bytes
Fast lookup, deduplicationset / frozenset
Structured datadict / dataclass / SimpleNamespace / UserDict / TypedDict
Use as dict keyfrozenset
Dot-access temporary objectsSimpleNamespace / dataclass
Count frequenciesCounter
Default values for keysdefaultdict
Efficient end operationsdeque
Custom list behaviorUserList
Custom dict behaviorUserDict
Binary databytes / bytearray / array.array
Configs with hierarchyChainMap
Named constantsEnum
Lazy sequencesrange / generators

Leave a Reply

Your email address will not be published. Required fields are marked *