Intermediate

Python Collections

Python offers four built-in container data types — list, dict, tuple and set — that streamline the process of storing, accessing and managing data collections in a flexible and intuitive...

Table of Contents

  1. Course presentation
  2. Using embedded containers
  1. Improve efficiency with advanced dictionaries
  1. Using Specialized Collections Classes
  1. Customizing built in data types

1. Course presentation

Python offers four built-in container data types — list, dict, tuple and set — that streamline the process of storing, accessing and managing data collections in a flexible and intuitive way. However, in some more complex data manipulation and storage scenarios, these basic types are not enough. This is where the collections module comes in.

This course covers the following topics:

  • Using built-in container data types (list, dict, tuple, set)
  • Managing specialized dictionary subclasses (defaultdict, OrderedDict, Counter)
  • Defining custom data structures with namedtuple
  • Managing scopes with ChainMap
  • Implementing queues and stacks with deque
  • Customizing built-in types with UserString, UserList and UserDict

At the end of this course, you will know how to use all the specialized container data types of the collections module.


2. Using built in containers

2.1 Container data types in Python

Container data types are data structures that can contain other objects. They allow you to organize, store and manipulate data in a structured way.

Python offers four built-in container data types:

TypeOrderlyMutableUnique ElementsAccess
listYesYesNoBy index
dictYes (Python 3.7+)YesUnique keysBy key
tupleYesNoNoBy index
setNoYesYesIteration / membership test
  • List: ordered collection of objects of mixed types. Mutable, dynamic, accessible by index.
  • Dictionary: collection of key-value pairs where each key is unique. Allows quick retrieval, addition and modification of values.
  • Tuple: ordered and immutable collection. Unable to add or remove items after creation.
  • Set: collection of unique and unordered elements. Ideal for membership tests and mathematical operations (intersection, union, etc.).

2.2 Lists

2.2.1 Basics of lists

Lists are one of the most used container data types in Python. They are :

  • Mutables: elements can be modified, added or deleted.
  • Dynamic: they can increase or decrease in size.
  • Ordered: the order of insertion of the elements is preserved.
  • Accessible by index: any element can be directly accessed via its index.

Creating a list

empty_list = []
print(empty_list)

books = ["1984", "Don Quixote", "The Great Gatsby"]
print(books)

Element access

The index of the first element is 0. Negative indexes allow you to access elements from the end.

movies = ["Inception", "The Matrix", "Interstellar"]
print(movies[0])  # Premier film
print(movies[1])  # Deuxième film
print(movies[-1]) # Dernier film

List length

languages = ["Python", "Rust", "C++"]
print(len(languages))

Add items

# Ajouter à la fin avec append()
shopping_list = ["milk", "apples", "bread"]
shopping_list.append("butter")
print(shopping_list)

# Insérer à une position précise avec insert()
primes = [2, 3, 5, 7]
primes.insert(2, 11)
print(primes)

Delete items

planets = ["Mercury", "Venus", "Earth", "Mars"]

# pop() supprime et retourne l'élément à l'index donné (dernier par défaut)
popped_planet_1 = planets.pop()      # Dernier élément
popped_planet_2 = planets.pop(1)     # pop() retourne l'élément supprimé
del planets[0]                        # Ne retourne pas l'élément supprimé

print(f"Removed Planet: {popped_planet_1}")
print(f"Removed Planet: {popped_planet_2}")
print(planets)

# remove() supprime la première occurrence d'une valeur
elements = ["Hydrogen", "Helium", "Lithium", "Beryllium"]
elements.remove("Lithium")
print(elements)

# clear() vide entièrement la liste
elements = ["Hydrogen", "Helium", "Lithium", "Beryllium"]
elements.clear()
print(elements)

Expand a list

colors = ["red", "green", "blue"]
more_colors = ["orange", "purple"]
colors.extend(more_colors)
print(colors)

Modify an element

continents = ["Asia", "Africa", "Europe"]
continents[2] = "Antarctica"
print(continents)

Count occurrences and find an index

notes = ["C", "D", "E", "F", "G", "A", "B", "C", "D", "C"]
print(notes.count("C"))

animals = ["lion", "tiger", "bear", "wolf"]
print(animals.index("bear"))

Sort a list

temperatures = [23, 18, 30, 15, 22]
print("Original order:", temperatures)
temperatures.sort()
print("Sorted in ascending order:", temperatures)
temperatures.sort(reverse=True)
print("Sorted in descending order:", temperatures)

# sorted() retourne une nouvelle liste sans modifier l'originale
sorted_temperatures_asc = sorted(temperatures)
print("Sorted in ascending order:", sorted_temperatures_asc)
sorted_temperatures_desc = sorted(temperatures, reverse=True)
print("Sorted in descending order:", sorted_temperatures_desc)
print("Original order:", temperatures)

Invert a list

colors = ["red", "green", "blue", "yellow"]
colors.reverse()
print("Reversed list:", colors)

Test membership

instruments = ["guitar", "piano", "violin"]
print("piano" in instruments)
print("drums" in instruments)

if "piano" in instruments:
    print("I can play piano")

Iterate over a list

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

Concatenation and repetition

list1 = [1, 2, 3]
list2 = [4, 5, 6]
concatenated_list = list1 + list2
repeated_list = list1 * 3
print("Concatenated:", concatenated_list)
print("Repeated:", repeated_list)

2.2.2 Slicing and copies

Slicing

Slicing allows you to extract a sublist. The syntax is list[start:stop:step].

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

# Avec les trois paramètres : start, stop et step
# numbers[start_index:stop_index:step]
even_numbers = numbers[1:8:2]
print("Even numbers:", even_numbers)

# Sans index de départ (commence au début)
first_three = numbers[:3]
print("First three numbers:", first_three)

# Sans index de fin (va jusqu'à la fin)
from_four_onwards = numbers[3:]
print("Numbers from 4 onwards:", from_four_onwards)

# Sans step (inclut chaque élément)
first_to_fourth = numbers[0:4]
print("First to fourth numbers:", first_to_fourth)

# Avec deux paramètres (start et stop)
middle_numbers = numbers[3:6]
print("Middle numbers (4, 5, 6):", middle_numbers)

# Avec des index négatifs
last_three = numbers[-3:]
print("Last three numbers:", last_three)

List copies — pitfalls to avoid

A common pitfall is to create a reference instead of a copy.

# Mauvaise façon : création d'une référence, pas d'une copie
planets_before_2006 = ["Mercury", "Venus", "Earth", "Mars",
                        "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]

# Cela crée une référence, pas une copie !
planets_after_2006 = planets_before_2006

planets_after_2006.pop()  # Modifie aussi planets_before_2006 !

print("Planets before 2006:", planets_before_2006)
print("Planets after 2006:", planets_after_2006)
# Bonne façon : utiliser copy() ou le slicing [:]
planets_before_2006 = ["Mercury", "Venus", "Earth", "Mars",
                        "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]

# Copie correcte avec .copy()
planets_after_2006_copy = planets_before_2006.copy()
# Copie correcte avec le slicing
planets_after_2006_slice = planets_before_2006[:]

planets_after_2006_copy.remove("Pluto")
planets_after_2006_slice.remove("Pluto")

print("Planets before 2006:", planets_before_2006)           # Pluto inclus
print("Planets after 2006 (using .copy()):", planets_after_2006_copy)   # Pluto retiré
print("Planets after 2006 (using slicing):", planets_after_2006_slice)  # Pluto retiré

2.2.3 Advanced methods on lists

List comprehensions

List comprehensions provide a concise syntax for creating lists.

squares = [x ** 2 for x in range(1, 11)]
print(squares)

squares_of_even = [x ** 2 for x in range(10) if x % 2 == 0]
print(squares_of_even)

Nested lists

Lists can contain other lists, allowing data structures to be represented in 2D or more.

2.2.4 Use cases for lists

Prototyping and inventory management

Lists are excellent for prototyping because of their simplicity.

# Inventory Management Example
inventory = [
    {"id": 1, "name": "T-shirt", "quantity": 25, "price": 15.99},
    {"id": 2, "name": "Jeans",   "quantity": 30, "price": 39.99},
    {"id": 3, "name": "Socks",   "quantity": 50, "price":  4.99}
]

def add_product(inventory, product):
    inventory.append(product)

def remove_product(inventory, product_id):
    inventory[:] = [product for product in inventory if product["id"] != product_id]

add_product(inventory, {"id": 4, "name": "Jacket", "quantity": 15, "price": 59.99})
remove_product(inventory, 2)

print("Current Inventory:")
for product in inventory:
    print(product)

Note: The [:] in inventory[:] = ... is important: it modifies the global list in place rather than creating a new local variable.

Managing a music playlist

playlist = []

def add_song(playlist, song):
    playlist.append(song)

def remove_song(playlist, song_title):
    playlist[:] = [song for song in playlist if song["title"] != song_title]

def move_song(playlist, song_title, new_position):
    for i, song in enumerate(playlist):
        if song["title"] == song_title:
            playlist.insert(new_position, playlist.pop(i))
            break

add_song(playlist, {"title": "The Entertainer", "artist": "Scott Joplin"})
add_song(playlist, {"title": "St. Louis Blues", "artist": "W.C. Handy"})
add_song(playlist, {"title": "Clair de Lune", "artist": "Claude Debussy"})
move_song(playlist, "Clair de Lune", 0)
remove_song(playlist, "St. Louis Blues")

print("Current Playlist:")
for song in playlist:
    print(f"{song['title']} by {song['artist']}")

Sensor data collection

sensor_data = []

def receive_sensor_data():
    import random
    return random.uniform(20, 30)

for _ in range(1000):
    new_data = receive_sensor_data()
    sensor_data.append(new_data)

recent_data = sensor_data[-100:]  # Les 100 dernières valeurs

if len(sensor_data) > 1000:
    sensor_data = sensor_data[-1000:]

average_recent = sum(recent_data) / len(recent_data)
print(f"Average of recent data: {average_recent}")

Real-time event logging

event_log = []

def receive_event(event):
    event_log.append(event)

for i in range(100):
    event = f"Event {i}"
    receive_event(event)

recent_events = event_log[-10:]
print("Recent Events:")
for event in recent_events:
    print(event)

Time complexity of list operations

import timeit

large_list = list(range(100000))

def access_element():
    _ = large_list[50000]

def append_element():
    large_list.append("new_element")

def remove_element():
    large_list.remove("new_element")

def insert_element():
    large_list.insert(50000, "inserted_element")

access_time = timeit.timeit(access_element, number=1000)
append_time = timeit.timeit(append_element, number=1000)
remove_time = timeit.timeit(remove_element, number=1000)
insert_time = timeit.timeit(insert_element, number=1000)

print(f"Access time: {access_time} seconds")
print(f"Append time: {append_time} seconds")
print(f"Remove time: {remove_time} seconds")
print(f"Insert time: {insert_time} seconds")
OperationComplexity
Access by indexO(1)
Append (end)O(1) amortized
Insert (middle)O(n)
RemoveO(n)
Search (in)O(n)

2.3 Dictionaries

2.3.1 Dictionary basics

Dictionaries are collections of key-value pairs where each key is unique. They are :

  • Mutable and dynamic: you can add, modify or delete pairs.
  • Ordered since Python 3.7: insertion order is preserved.
  • Optimized for search: key access is O(1).

Creating a dictionary

# Dictionnaire vide
my_dict = {}
print(my_dict)

# Dictionnaire avec des valeurs initiales
person_info = {"name": "Some name", "age": 30, "city": "Some city"}
print(person_info)

Element access

fruit = {"apple": 2, "banana": 3}

print("Price of apple:", fruit["apple"])
print("Price of banana:", fruit["banana"])

# get() retourne None (ou la valeur par défaut) si la clé n'existe pas
print("Price of apple:", fruit.get("apple", "Price not found"))
print("Price of mango:", fruit.get("mango", "Price not found"))

Adding and updating elements

my_dict = {}
# Ajout d'une nouvelle paire clé-valeur
my_dict["language"] = "Python"
# Mise à jour d'une valeur existante
my_dict["language"] = "JavaScript"
# Ajout de plusieurs éléments
my_dict.update({"version": "ES6", "typing": "dynamic"})
# Opérateur de mise à jour |=
# my_dict |= other_dict

# Fusion de deux dictionnaires avec l'opérateur de fusion
merged_dict = my_dict | {"new_key": "new_value", "version": "ES7"}
print("my_dict:", my_dict)
print("merged_dict:", merged_dict)

Deleting items

my_dict = {"language": "Python", "release_year": "2019",
           "version": "3.8", "platform": "Windows"}

del my_dict["platform"]                # Suppression directe
version = my_dict.pop("version")       # Suppression et retour de la valeur
release_year = my_dict.popitem()       # Suppression et retour du dernier élément
my_dict.clear()                        # Vidage complet

print("Removed version:", version)
print("Release year element:", release_year)
print("Current dictionary:", my_dict)

Iteration over a dictionary

person_info = {"name": "Some name", "age": 28, "city": "Some city"}

# Itération sur les clés et valeurs
for key, value in person_info.items():
    print(f"{key}: {value}")

# Itération sur les clés uniquement
for key in person_info.keys():
    print(f"Key: {key}")

# Itération sur les valeurs uniquement
for value in person_info.values():
    print(f"Value: {value}")

Checking for the existence of a key

my_dict = {"language": "Python", "version": "3.9"}

if "version" in my_dict:
    print("Version found:", my_dict["version"])
else:
    print("Version key does not exist.")

2.3.2 Advanced methods on dictionaries

Dictionary comprehensions

# Création d'un dictionnaire avec une comprehension
squares = {x: x*x for x in range(6)}
print("Squares:", squares)

# Transformation d'un dictionnaire (inversion clés/valeurs)
original_dict = {"a": 1, "b": 2, "c": 3, "d": 4}
inverted_dict = {value: key for key, value in original_dict.items()}
print("Inverted dict:", inverted_dict)

Copying dictionaries

As with lists, a simple assignment creates a reference, not a copy.

import copy

# Dictionnaire original
original_dict = {"name": "Some name", "hobbies": ["reading", "traveling"]}

# Copie superficielle (shallow copy)
shallow_copied_dict = original_dict.copy()
shallow_copied_dict["age"] = 30

# Copie profonde (deep copy)
deep_copied_dict = copy.deepcopy(original_dict)

# Modification des copies
shallow_copied_dict["hobbies"].append("hiking")
deep_copied_dict["hobbies"].append("swimming")

print("Original Dict:", original_dict)
print("Shallow Copied Dict:", shallow_copied_dict)
print("Deep Copied Dict:", deep_copied_dict)

Warning: copy() only copies references to nested mutable objects (like lists). copy.deepcopy() creates independent copies of all nested objects.

The setdefault() method

my_dict = {"a": 1, "b": 2}

# "a" existe, retourne la valeur de "a"
value_a = my_dict.setdefault("a", 99)
print("Value of a:", value_a)
print("Dictionary:", my_dict)

# "c" n'existe pas, ajoute "c" avec la valeur par défaut 99
value_c = my_dict.setdefault("c", 99)
print("Value of c:", value_c)
print("Dictionary:", my_dict)
# Groupement par première lettre avec setdefault
fruit = ["apple", "pear", "banana", "apricot", "blueberry", "orange"]

groups = {}
for item in fruit:
    key = item[0]
    groups.setdefault(key, []).append(item)

print(groups)

Sorting a dictionary

from operator import itemgetter

scores = {"KeyC": 42, "KeyA": 25, "KeyB": 162}
print("scores.items():", scores.items())

# Tri par clé
sorted_by_key = dict(sorted(scores.items()))
print("Sorted by key:", sorted_by_key)

# Tri par valeur
sorted_by_value = dict(sorted(scores.items(), key=itemgetter(1)))
print("Sorted by value:", sorted_by_value)

2.3.3 Dictionaries use cases

Configuration parameters

config = {
    "debug_mode": True,
    "api_endpoint": "https://api.example.com",
    "retry_attempts": 3,
    "themes": ["light", "dark"]
}

debug_mode = config["debug_mode"]
print(f"Debug Mode: {debug_mode}")

config["retry_attempts"] = 5
print(f"Retry Attempts: {config['retry_attempts']}")

Dispatch tables (Function Dispatch Tables)

import os
import platform

def list_files(directory='.'):
    """Lists files in the given directory."""
    files = [f for f in os.listdir(directory)
             if os.path.isfile(os.path.join(directory, f))]
    for file in files:
        print(file)

def count_files(directory='.'):
    """Counts the number of files in the given directory."""
    files = [f for f in os.listdir(directory)
             if os.path.isfile(os.path.join(directory, f))]
    print(f"Number of files: {len(files)}")

def sys_info():
    """Displays basic system information."""
    print(f"System: {platform.system()}")
    print(f"Version: {platform.version()}")

# Table de dispatch
commands = {
    "list_files":  list_files,
    "count_files": count_files,
    "sys_info":    sys_info,
}

user_command = input("Enter command (list_files, count_files, sys_info): ").strip()
directory = '.'

if user_command in commands:
    if user_command in ["list_files", "count_files"]:
        commands[user_command](directory)
    else:
        commands[user_command]()
else:
    print("Unknown command.")

Memoization

# Mémoïsation de la fonction de Fibonacci
def fibonacci(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 2:
        return 1
    memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
    return memo[n]

print(fibonacci(10))

Plugin Registry

plugins = {}

def register_plugin(name, func):
    plugins[name] = func

def call_plugin(name):
    if name in plugins:
        plugins[name]()

def plugin_greet():
    print("Hello from the plugin!")

register_plugin("greeting_plugin", plugin_greet)
call_plugin("greeting_plugin")

Counting and grouping (frequency tables)

log_data = [
    {"ip": "192.168.1.1", "url": "/index.html",    "status": "200"},
    {"ip": "192.168.1.2", "url": "/about.html",    "status": "200"},
    {"ip": "192.168.1.1", "url": "/contact.html",  "status": "200"},
    {"ip": "192.168.1.3", "url": "/index.html",    "status": "200"},
    {"ip": "192.168.1.2", "url": "/products.html", "status": "200"},
    {"ip": "192.168.1.1", "url": "/products.html", "status": "404"},
]

request_counts = {}
for log_entry in log_data:
    ip_address = log_entry["ip"]
    request_counts[ip_address] = request_counts.get(ip_address, 0) + 1

for ip, count in request_counts.items():
    print(f"IP Address {ip} made {count} requests")

The **kwargs parameter

def html_tag(tag, content, **kwargs):
    """
    Returns a string representation of an HTML tag.
    """
    print("kwargs:", kwargs)
    attributes = ' '.join([f'{key}="{value}"' for key, value in kwargs.items()])
    return f'<{tag} {attributes}>{content}</{tag}>'

print(html_tag('a', 'Click Here', href="https://example.com", style="color: red;"))

2.4 Tuples

2.4.1 Tuple bases

Like lists, tuples are used to store objects in an ordered sequence. But unlike lists, tuples are immutable: you cannot modify, add or delete elements after their creation. Tuples are also more memory efficient.

Creating tuples

# Tuple vide
my_tuple = ()
print(my_tuple)

# Tuple avec éléments
my_tuple = ("String", 2, 3.5)
print(my_tuple)

# Tuple à un seul élément (la virgule est obligatoire !)
my_tuple = ("String",)
print(my_tuple)

# Sans parenthèses (syntaxe valide mais déconseillée dans ce contexte)
my_tuple = "String", 2, 3.5
print(my_tuple)

Element access

my_tuple = ('p','e','r','m','i','t')
print(my_tuple[0])   # p
print(my_tuple[5])   # t
print(my_tuple[-1])  # t
print(my_tuple[-6])  # p

Immutability

my_tuple = ('p','e','r','m','i','t')

try:
    my_tuple[3] = 'a'
except TypeError as e:
    print("Error: Cannot modify a tuple. Tuples are immutable.")

Tuple methods

my_tuple = (1, 2, 3, 1, 2, 1, 2, 3, 1)
print(my_tuple.count(1))   # Nombre d'occurrences de 1
print(my_tuple.index(3))   # Index de la première occurrence de 3

Packing and unpacking

# Packing
my_tuple = 3.14, "Python", "Tuples"

# Unpacking
pi, language, concept = my_tuple

print(pi)
print(language)
print(concept)

2.4.2 Use cases for tuples

Immutable collection of related elements

Geographic coordinates are a good example: longitude and latitude should always go together and should never be changed.

def calculate_distance(coord1, coord2):
    return ((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2) ** 0.5

# Coordonnées en tant que tuples
point_a = (40.7128, -74.0060)    # New York
point_b = (34.0522, -118.2437)   # Los Angeles

distance = calculate_distance(point_a, point_b)
print(f"Distance: {distance} units")

Return multiple values ​​from a function

Python does not support returning multiple values ​​natively — it actually returns a single object, a tuple. Packing and unpacking allow you to simulate this behavior.

def min_max(numbers):
    return min(numbers), max(numbers)  # Retourne un tuple

min_result, max_result = min_max([1, 20, 33, 401, 5])
print(f"Min is {min_result} and max is {max_result}")

The *args parameter

def sum_numbers(*args):
    print(args)
    return sum(args)

print(sum_numbers(1, 2, 3))
print(sum_numbers(1, 2, 3, 4, 5))

Storing configuration constants

ENV_VARIABLES = ('DB_HOST', 'DB_USER', 'DB_PASS', 'API_KEY')

import os
db_host = os.getenv(ENV_VARIABLES[0])

Working with database records

def get_employee_record(employee_id):
    # Simulation de la récupération d'un enregistrement
    return (123, "John Doe", "Software Engineer", 75000)

employee_record = get_employee_record(123)
print(f"Employee Record: ID={employee_record[0]}, Name={employee_record[1]}, "
      f"Role={employee_record[2]}, Salary={employee_record[3]}")

2.5 Sets

2.5.1 Set basics

Sets are data structures designed to store and manipulate collections of unique elements. They :

  • Automatically remove duplicates.
  • Are dynamic: they can increase or decrease.
  • Are unordered: no access by index, no slicing.
  • Cannot contain mutable objects (no dict or list inside).

Creating sets

# Utiliser le constructeur set() pour créer un set vide
my_set = set()
print(my_set)

# Avec des accolades (les doublons sont automatiquement supprimés)
my_set = {1, 2, 3, "Python", 1, 2, 3}
print(my_set)

# Depuis un itérable
my_set = set([1, 2, 3])
print(my_set)

Warning: {} alone creates an empty dictionary, not an empty set.

Element access

my_set = {1, 2, 3, 4}

for element in my_set:
    print(element)

print(1 in my_set)
print(5 in my_set)

Modification of a set

my_set = {1, 2, 3, 4}

my_set.add("element")
print(my_set)

my_set.remove("element")
my_set.discard(5)  # Ne lève pas d'erreur si l'élément n'existe pas
print(my_set)

Set operations

set1 = {1, 2, 3}
set2 = {3, 4, 5}
print("Set 1:", set1)
print("Set 2:", set2)

print("Union:", set1 | set2)              # Éléments des deux sets sans doublons
print("Intersection:", set1 & set2)       # Éléments présents dans les deux sets
print("Difference:", set1 - set2)         # Éléments de set1 absents de set2
print("Symmetric Difference:", set1 ^ set2)  # Éléments des deux sets sans l'intersection

# Les méthodes équivalentes (acceptent n'importe quel itérable)
# print(set1.union(set2))
# print(set1.intersection(set2))
# print(set1.difference(set2))
# print(set1.symmetric_difference(set2))

Augmented assignment operators

# Modifient le premier set en place
# set1 |= set2
# set1 &= set2
# set1 -= set2
# set1 ^= set2

Subsets and supersets

setA = {1, 2, 3}
setB = {1, 2, 3, 4, 5}
setC = {10, 11, 12}

print("setA is a subset of setB:", setA.issubset(setB))     # True
# Équivalent : setA <= setB
# Sous-ensemble propre : setA < setB

print("setB is a superset of setA:", setB.issuperset(setA)) # True
# Équivalent : setB >= setA

# Deux sets sont disjoints quand ils n'ont aucun élément en commun
print("setA has no intersection with setC:", setA.isdisjoint(setC))
print("setA has no intersection with setB:", setA.isdisjoint(setB))

Frozen sets

Frozen sets are immutable sets.

my_set = frozenset([1, 2, 3])

try:
    my_set.add(4)
except AttributeError as error:
    print(f"Error: {error}")

2.5.2 Use cases for sets

Removing duplicates

raw_hashtags = ['#python', '#dev', '#python', '#coding',
                '#tech', '#dev', '#coding', '#tech', '#pluralsight']

unique_hashtags = set(raw_hashtags)
print(unique_hashtags)

Effective membership test

Sets provide O(1) complexity for membership testing, making them much faster than lists for large collections.

import timeit

user_ids = list(range(1, 1000001))
test_id = 999999

user_ids_set = set(user_ids)

def check_list():
    return test_id in user_ids

def check_set():
    return test_id in user_ids_set

list_time = timeit.timeit(check_list, number=100)
set_time = timeit.timeit(check_set, number=100)

print(f"List membership test time: {list_time:.5f} seconds")
print(f"Set membership test time: {set_time:.5f} seconds")

Set operations for data analysis

interests_group1 = {"reading", "traveling", "cooking", "gardening"}
interests_group2 = {"traveling", "music", "video games", "cooking"}

# Intérêts communs
common_interests = interests_group1.intersection(interests_group2)
print(f"Common interests: {common_interests}")

# Intérêts uniques au groupe 1
unique_group1 = interests_group1.difference(interests_group2)
print(f"Unique to Group 1: {unique_group1}")

# Intérêts uniques au groupe 2
unique_group2 = interests_group2.difference(interests_group1)
print(f"Unique to Group 2: {unique_group2}")

2.6 Module 2 Summary

  • Lists: dynamic tables that can store data of mixed types. Mutable — they allow their elements to be added, deleted, and modified. Found in most applications, their use cases range from prototyping to raw data preparation.
  • Dictionaries: collections of key-value pairs allowing fast retrieval by key. Useful for managing configuration parameters, implementing frequency tables, dispatch tables, etc.
  • Tuples: immutable. Provide a reliable container for fixed data sequences, often used to store records or return multiple values ​​from a function.
  • Sets: Unordered collections of unique elements, known for their rapid membership testing, automatic elimination of duplicates, and mathematical set operations.

3. Improve efficiency with advanced dictionaries

The collections module provides several specialized subclasses of the built-in dict type. These classes meet specific needs that ordinary dictionaries do not cover optimally.

3.1 defaultdict

3.1.1 Understanding defaultdict

defaultdict is a subclass of the built-in dict. It automatically provides a default value whenever we try to access a key that does not exist.

from collections import defaultdict

print(issubclass(defaultdict, dict))  # True

# Utiliser list() comme factory function
dd = defaultdict(list)

The factory function is called without arguments to provide the default value. It must be callable.

Adding elements to a defaultdict

from collections import defaultdict

dd = defaultdict(list)

dd["key1"].append(1)
dd["key2"].append(2)
print(dd["key3"])  # [] — clé créée automatiquement avec une liste vide
print(dd)

defaultdict vs setdefault method

from collections import defaultdict

std_dict = {}
std_dict.setdefault("key", "Default")
print(std_dict["key"])  # Default

dd = defaultdict(lambda: "Default")
print(dd["key"])        # Default

defaultdict is slightly faster for handling missing keys. Additionally, the factory function of defaultdict is only called when the key does not exist, while setdefault always evaluates its second argument.

from timeit import timeit

setup_defaultdict_append = """
from collections import defaultdict
dd = defaultdict(list)
"""
stmt_defaultdict_append = "[dd[f'key_{i // 2}'].append(1) for i in range(2000000)]"
time_defaultdict_append = timeit(stmt=stmt_defaultdict_append,
                                  setup=setup_defaultdict_append, number=10)

setup_setdefault_append = "std_dict = {}"
stmt_setdefault_append = "[std_dict.setdefault(f'key_{i // 2}', []).append(1) for i in range(2000000)]"
time_setdefault_append = timeit(stmt=stmt_setdefault_append,
                                 setup=setup_setdefault_append, number=10)

print("defaultdict:", time_defaultdict_append)
print("setdefault:", time_setdefault_append)

Behavior of the factory function

from collections import defaultdict

def long_factory_function(trigger):
    print(f"Factory function ran by {trigger}")
    return []

dd = defaultdict(lambda: long_factory_function("defaultdict"), {"existing_key": []})
std_dict = {"existing_key": []}

dd["existing_key"].append(1)
# La factory n'est PAS appelée car "existing_key" existe déjà

std_dict.setdefault("existing_key", long_factory_function("setdefault")).append(1)
# long_factory_function EST appelée même si "existing_key" existe déjà

3.1.2 Use cases of defaultdict

Grouping of elements

from collections import defaultdict

items = [
    ("Apple",       "Fruit"),
    ("Banana",      "Fruit"),
    ("Hammer",      "Tool"),
    ("Screwdriver", "Tool"),
    ("Laptop",      "Electronics"),
    ("Smartphone",  "Electronics")
]

dd = defaultdict(list)
for item, item_type in items:
    dd[item_type].append(item)
    # Avec un dict ordinaire : std_dict.setdefault(item_type, []).append(item)

print(dd)

Unique element grouping (no duplicates)

from collections import defaultdict

items = [
    ("Apple",    "Fruit"),
    ("Banana",   "Fruit"),
    ("Banana",   "Fruit"),   # Doublon
    ("Banana",   "Fruit"),   # Doublon
    ("Hammer",   "Tool"),
    ("Hammer",   "Tool"),    # Doublon
    ("Screwdriver", "Tool"),
    ("Laptop",   "Electronics"),
    ("Smartphone", "Electronics")
]

dd = defaultdict(set)  # set comme factory — ignore les doublons automatiquement
for item, item_type in items:
    dd[item_type].add(item)

print(dd)

Counting items by category

from collections import defaultdict

items = [
    ("Apple",       "Fruit"),
    ("Banana",      "Fruit"),
    ("Hammer",      "Tool"),
    ("Screwdriver", "Tool"),
    ("Laptop",      "Electronics"),
    ("Smartphone",  "Electronics")
]

dd = defaultdict(int)  # int() retourne 0 par défaut
for _, item_type in items:
    dd[item_type] += 1

print(dd)

Calculation of the total amount by category

from collections import defaultdict

items = [
    ("Apple",       5,    "Fruit"),
    ("Banana",      3,    "Fruit"),
    ("Hammer",      10,   "Tool"),
    ("Screwdriver", 10,   "Tool"),
    ("Laptop",      5000, "Electronics"),
    ("Smartphone",  4000, "Electronics")
]

dd = defaultdict(int)
for _, price, item_type in items:
    dd[item_type] += price

for item_type, price in dd.items():
    print(f'Total sum for {item_type} items is: {price}')

3.2 OrderedDict

3.2.1 Understanding OrderedDict

OrderedDict is a subclass of dict that maintains the insertion order of keys. Since Python 3.7, ordinary dictionaries also preserve insertion order, but OrderedDict provides additional functionality.

Creating an OrderedDict

from collections import OrderedDict

print(issubclass(OrderedDict, dict))  # True

# Dictionnaire ordonné vide
od = OrderedDict()
print(od)

# Depuis un itérable de tuples
od = OrderedDict([("key1", "value1"), ("key2", "value2")])
print(od)

# Depuis un dictionnaire ordinaire
od = OrderedDict({"key1": "value1", "key2": "value2"})
print(od)

# Avec des keyword arguments
od = OrderedDict(key1="value1", key2="value2")
print(od)

Comparison of two OrderedDict

The main difference with ordinary dictionaries: two OrderedDict are equal only if their keys are in the same order.

from collections import OrderedDict

od1 = OrderedDict({"a": 1, "b": 2})
od2 = OrderedDict({"b": 2, "a": 1})
print(od1 == od2)  # False — ordre différent !

# Deux dict ordinaires seraient égaux :
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2)  # True

Moving elements

from collections import OrderedDict

od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
od.move_to_end("b")         # Déplace "b" à la fin
print(od)

od.move_to_end("b", last=False)  # Déplace "b" au début
print(od)

Delete from both ends

from collections import OrderedDict

od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
print(od)

last_item = od.popitem()           # Supprime le dernier élément
print(f"Popped last item: {last_item}")
print(od)

first_item = od.popitem(last=False)  # Supprime le premier élément
print(f"Popped first item: {first_item}")
print(od)

3.2.2 OrderedDict use cases

Managing a sequence of ordered operations

from collections import OrderedDict

operations = OrderedDict([
    ("clean_data",   "Clean the loaded data"),
    ("load_data",    "Load data from source"),
    ("analyze_data", "Analyze the cleaned data"),
    ("save_results", "Save analysis results")
])

# Prioriser une opération spécifique
operations.move_to_end("load_data", last=False)

# Exécuter et supprimer la première opération
first_operation_key, _ = operations.popitem(last=False)
print(f"Running '{first_operation_key}' operation.")

Checking order in tests

from collections import OrderedDict

expected_sequence = OrderedDict([
    ("baseline", "Original version without changes"),
    ("change1",  "Increased font size for better readability"),
    ("change2",  "Changed call-to-action button color"),
    ("final",    "Added customer testimonials")
])

actual_sequence_test = OrderedDict([
    ("baseline", "Original version without changes"),
    ("change2",  "Changed call-to-action button color"),
    ("change1",  "Increased font size for better readability"),
    ("final",    "Added customer testimonials")
])

def is_sequence_correct(expected, actual):
    return expected == actual

print("Test 1 sequence correct:", is_sequence_correct(expected_sequence, actual_sequence_test))
# False — l'ordre de change1 et change2 est inversé

Backward compatibility

Before Python 3.7, ordinary dictionaries did not guarantee insertion order. If you are maintaining legacy codebases, OrderedDict guarantees the expected behavior.


3.3 Counter

3.3.1 Understanding Counter

Counter is a subclass of dict specially designed for counting hashable objects. The elements are stored as dictionary keys, and their frequency (count) is the value.

from collections import Counter

print(issubclass(Counter, dict))  # True

# Initialisation depuis une liste
letters = ["a", "b", "c", "a", "c", "a", "b", "c"]
letter_counter = Counter(letters)
print("Counter from a list:\t", letter_counter)

# Initialisation depuis une chaîne
string_letter_counter = Counter("banana")
print("Counter from a string:\t", string_letter_counter)

# Initialisation avec des comptes explicites
dict_letter_counter = Counter({"a": 4, "b": 2, "c": -1})
print("Initialized Counter:\t", dict_letter_counter)

Element access

fruits = ["apple", "banana", "cherry", "apple", "cherry"]
fruit_counter = Counter(fruits)

print(fruit_counter["apple"])  # 2
print(fruit_counter["pear"])   # 0 (pas d'erreur KeyError)

Updating accounts

letters = ["a", "b", "c", "a", "c", "a", "b", "c"]
letter_counter = Counter(letters)
print("Original counter:\t\t\t\t", letter_counter)

# Mise à jour avec une chaîne ou une liste
letter_counter.update("aa")
letter_counter.update(["c", "c"])
print("Updated counter with strings and lists:\t\t", letter_counter)

# Mise à jour avec un dictionnaire ou des keyword arguments
letter_counter.update({"b": 3})
letter_counter.update(a=2, b=2, c=2)
print("Updated counter with dictionaries and kw args:\t", letter_counter)

# Soustraction
letter_counter.subtract(a=7, b=6, c=8)
print("Subtracted counter:\t\t\t\t", letter_counter)

# Réinitialisation
letter_counter.clear()
print("Reset Counter:\t\t\t\t\t", letter_counter)

Counter Operators

c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)

# Addition (ne garde que les comptes positifs)
print("Addition:\t\t", c1 + c2)
# Soustraction (ne garde que les comptes positifs)
print("Subtraction:\t\t", c1 - c2)

# Intersection (minimum des comptes)
print("Intersection (min):\t", c1 & c2)
# Union (maximum des comptes)
print("Union (max):\t\t", c1 | c2)

# Opérations unaires (zéro toujours exclu)
print("Positive counts:\t", +c1)          # Comptes > 0
print("Negative counts:\t", -Counter(a=1, b=-2))  # Comptes < 0

print("c1 == c2", c1 == c2)

Counter Methods

c = Counter(a=2, b=3, c=4)

# Reconstruction de la séquence originale (sans l'ordre original)
print("c.elements():\t\t", list(c.elements()))

# Liste des éléments les plus communs
print("c.most_common():\t", c.most_common())
print("c.most_common(2):\t", c.most_common(2))

# Total de tous les comptes
print("c.total():\t\t", c.total())

3.3.2 Counter use cases

Word frequency counting

from collections import Counter

text = "a quick brown fox jumps over the lazy dog"
words = text.split()
word_counts = Counter(words)

print(word_counts)

Find the most common elements

visits = ["home", "about", "contact", "home", "about", "home",
          "profile", "home", "about", "contact"]
visit_counts = Counter(visits)

most_visited = visit_counts.most_common(2)
least_visited = visit_counts.most_common()[:-3:-1]
print("Most visited pages:", most_visited)
print("Least visited pages:", least_visited)

Inventory management

from collections import Counter

inventory_a = Counter(apples=3, oranges=2)
inventory_b = Counter(apples=1, bananas=2, oranges=1)

# Combiner les inventaires
total_inventory = inventory_a + inventory_b
print("Total inventory:\t\t", total_inventory)

# Articles vendus
sold_items = Counter(apples=2, bananas=1)
remaining_inventory = total_inventory - sold_items
print("Inventory after the sale:\t", remaining_inventory)

Analysis of voting results

votes = ["CandidateA", "CandidateB", "CandidateB", "CandidateA",
         "CandidateB", "CandidateB", "CandidateC"]
vote_counts = Counter(votes)

print(vote_counts)

Implementation of multisets (first factorization)

A multiset (or bag) is a modification of the concept of a set that allows multiple instances of the same element. Counter is essentially a multiset implementation.

from collections import Counter
import math

def prime_factorization(n):
    factors = Counter()
    divisor = 2
    while divisor**2 <= n:
        while n % divisor == 0:
            factors[divisor] += 1
            n //= divisor
        divisor += 1
    if n > 1:
        factors[n] += 1
    return factors

n = 2376
prime_factors = prime_factorization(n)
factors_str = f"{n} = " + " x ".join([f"{factor}^{power}"
                                       for factor, power in prime_factors.items()])
print(prime_factors)
print("Prime factorization:", factors_str)

# Recalculer le nombre à partir des facteurs premiers
number = math.prod(prime_factors.elements())
print("Number:", number)

3.4 Module 3 Summary

  • defaultdict: subclass of dict that returns a default value for missing keys. The default value is specified by the factory function provided at creation. Eliminates the need to check for the existence of keys to initialize lists or counters.
  • OrderedDict: subclass of dict which remembers the insertion order. Useful for order-aware comparisons and maintaining old codebases. Offers move_to_end() and a popitem() capable of removing from both ends.
  • Counter: subclass of dict designed to count hashable objects. Particularly useful for frequency analysis and algorithms requiring counting.

4. Using Specialized Collections Classes

4.1 namedtuple()

4.1.1 Understanding namedtuple()

namedtuple() is a factory function of the collections module which returns a new tuple subclass with named fields. It combines the immutability of tuples with the readability of attribute access.

Distinction between class and alias

class Point:
    pass

Point2 = Point
print(Point.__name__)   # Point
print(Point2.__name__)  # Point aussi — Point2 est juste un alias

Creating a namedtuple

from collections import namedtuple

# La factory function retourne une nouvelle classe nommée "Pixel"
Pixel = namedtuple("Pixel", "red green blue")
# Syntaxes équivalentes :
# Pixel = namedtuple("Pixel", "red, green, blue")
# Pixel = namedtuple("Pixel", ["red", "green", "blue"])
# Pixel = namedtuple("Pixel", (field for field in ["red", "green", "blue"]))

# Instanciation
pixel = Pixel(red=255, green=50, blue=0)
print(pixel)

# Obtenir les noms des champs
print(Pixel._fields)

Element access

from collections import namedtuple

Pixel = namedtuple("Pixel", "red green blue")
pixel = Pixel(red=255, green=50, blue=0)

print("Accessing values by indices:")
print(pixel[0])
print(pixel[1])
print(pixel[2])

print("Accessing values by field names with the dot syntax:")
print(pixel.red)
print(pixel.green)
print(pixel.blue)

Optional arguments of namedtuple()

from collections import namedtuple

# L'argument 'rename' : renomme automatiquement les champs invalides
user_fields = ["username", "_password", "username", "as"]
User = namedtuple("User", user_fields, rename=True)
print("The 'rename' argument")
print("User._fields:", User._fields)

# L'argument 'defaults' : valeurs par défaut pour les derniers champs
Dog = namedtuple("Dog", ["name", "age", "location"], defaults=[0, "Home"])
dog = Dog("Balto")
print("\nThe 'defaults' argument")
print("dog:", dog)
print("dog._field_defaults:", dog._field_defaults)

# L'argument 'module' : définit __module__ de la classe
Item = namedtuple("Item", ["name"], module="my_module")
print("\nThe 'module' argument")
print("Item.__module__:", Item.__module__)

Construction from iterables with _make()

from collections import namedtuple

Pixel = namedtuple("Pixel", "red green blue")

image_pixel_data = [
    [255, 43, 22],
    [230, 44, 23],
    [230, 44, 23]
]

sprite = [Pixel._make(pixel) for pixel in image_pixel_data]
print(sprite)

Conversion to and from dictionary

from collections import namedtuple

Pixel = namedtuple("Pixel", "red green blue")

# Depuis un dictionnaire avec le déballage (**)
pixel = Pixel(**{"red": 255, "green": 50, "blue": 0})
print(pixel)

# Vers un dictionnaire
print(pixel._asdict())

Updating fields with _replace()

As namedtuples are immutable, _replace() creates a new instance with the modified fields.

from collections import namedtuple

Dog = namedtuple("Dog", ["name", "age", "location"])
dog = Dog("Hachiko", 11, "Shibuya Station")

dog = dog._replace(name="Scooby-Don't")
print(dog)

4.1.2 Use cases of namedtuple()

Immutable container with named fields

Namedtuples improve code readability. Compared to (mutable) data classes and dictionaries, namedtuples offer better performance and a reduced memory footprint.

from collections import namedtuple

City = namedtuple("City", ["name", "latitude", "longitude"])

cities = [
    City("New York",     40.7128,  -74.0060),
    City("Los Angeles",  34.0522, -118.2437),
    City("Chicago",      41.8781,  -87.6298),
]

def find_city_by_name(city_name):
    for city in cities:
        if city.name == city_name:
            return city
    return None

found_city = find_city_by_name("Chicago")
if found_city:
    print(f"The coordinates of {found_city.name} are "
          f"({found_city.latitude}, {found_city.longitude})")

Using typing.NamedTuple for type hints and custom methods

from typing import NamedTuple

class City(NamedTuple):
    """
    Represents a city with a name and its geographic coordinates.

    Attributes:
        name (str): The name of the city.
        latitude (float): The latitude of the city.
        longitude (float): The longitude of the city.
    """
    name: str
    latitude: float
    longitude: float

    def __str__(self):
        return (f"The city of {self.name} can be found at "
                f"({self.latitude}, {self.longitude}) coordinates.")

city = City("New York", 40.7128, -74.0060)
print(city)

Reduce the number of parameters of a function

from collections import namedtuple

CustomerInfo = namedtuple("CustomerInfo",
    ["id", "first_name", "last_name", "email",
     "address", "city", "state", "zip_code"])

def process_customer_info(customer_info):
    print(f"Processing {customer_info.first_name} {customer_info.last_name} "
          f"living in {customer_info.city}, {customer_info.state}.")

customer = CustomerInfo(1, "X", "Y", "x.y@example.com",
                        "123 Elm St", "Anytown", "Anystate", "12345")
process_customer_info(customer)

Return a namedtuple from a function

from collections import namedtuple

FinancialStats = namedtuple("FinancialStats",
    ["average_expense", "total_expense", "highest_expense"])

def calculate_financial_stats(expenses):
    total_expense = sum(expenses)
    average_expense = total_expense / len(expenses)
    highest_expense = max(expenses)
    return FinancialStats(average_expense, total_expense, highest_expense)

expenses = [250, 320, 150, 400, 500]
stats = calculate_financial_stats(expenses)
print(f"Average Expense: ${stats.average_expense:.2f}")
print(f"Total Expense: ${stats.total_expense}")
print(f"Highest Expense: ${stats.highest_expense}")

4.2 ChainMap

4.2.1 Understanding ChainMap

ChainMap is a class that allows you to group several mappings (dictionaries) into a single logical view. It is particularly useful for managing multiple scopes and creates an updated view that acts as an access interface to multiple linked dictionaries.

ChainMap does not merge dictionaries — it stores references to those dictionaries in an internal list.

Creating a ChainMap

from collections import ChainMap

dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
chain_map = ChainMap(dict1, dict2)

print(chain_map)
# Modifier les dictionnaires originaux affecte le ChainMap
dict1["a"] = 20
print(chain_map)

Access to values

from collections import ChainMap

dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4, "d": 5}
chain_map = ChainMap(dict1, dict2)
print(chain_map)

print("chain_map['a']:", chain_map["a"])  # Depuis dict1
print("chain_map['b']:", chain_map["b"])  # Depuis dict1 (priorité sur dict2)
print("chain_map['c']:", chain_map["c"])  # Depuis dict2 (absent de dict1)

Keys from first dictionary take precedence. If a key exists in several dictionaries, the value of the first is returned.

Mutations

from collections import ChainMap

dict1 = {"a": 1, "b": 2, "z": 10}
dict2 = {"b": 3, "c": 4, "d": 5}
cm = ChainMap(dict1, dict2)

# Les mises à jour affectent le premier mapping
cm["a"] = 100
cm["b"] = 200

# Ajouter une nouvelle clé va dans le premier mapping
cm["c"] = 400

# Seules les suppressions depuis le premier mapping sont possibles
cm.pop("z")
# clear() vide également seulement le premier mapping

print(cm)

Addition and deletion of the first mapping

from collections import ChainMap

dict1 = {"a": 1, "b": 2, "z": 10}
dict2 = {"b": 3, "c": 4, "d": 5}
cm = ChainMap(dict1, dict2)

# new_child() retourne un nouveau ChainMap avec le nouveau mapping en tête
dict3 = {"e": 6, "f": 7}
cm2 = cm.new_child(dict3)
print(cm2)

# parents retourne un nouveau ChainMap sans le premier mapping
cm3 = cm2.parents
print(cm3)

The maps attribute

from collections import ChainMap

dict1 = {"a": 1, "b": 2, "z": 10}
dict2 = {"b": 3, "c": 4, "d": 5}
cm = ChainMap(dict1, dict2)

print("cm.maps", cm.maps)

# Inverser l'ordre des mappings
cm.maps.reverse()
print("cm.maps", cm.maps)

# Supprimer le dernier dictionnaire
cm.maps.pop()
print("cm.maps", cm.maps)

Iteration

from collections import ChainMap

dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4, "d": 5}
chain_map = ChainMap(dict1, dict2)

# L'itération se fait du dernier mapping vers le premier
for key, value in chain_map.items():
    print(key, value)
# Même ordre pour .keys() et .values()

4.2.2 ChainMap use cases

Application configuration with priorities

from collections import ChainMap
import os

# Configuration par défaut
default_config = {"theme": "Default", "language": "English", "show_ads": True}

# Les variables d'environnement ont la priorité sur les paramètres par défaut
env_config = os.environ

app_config = ChainMap(env_config, default_config)
print("Theme:", app_config["theme"])

# La configuration utilisateur a la priorité maximale
user_config = {"theme": "Dark Mode", "show_ads": False}
app_config = app_config.new_child(user_config)

print("\nAfter adding the user config")
print("Theme:", app_config["theme"])
print("Language:", app_config["language"])
print("Show Ads:", app_config["show_ads"])

CLI argument handling

import argparse
from collections import ChainMap

def main():
    parser = argparse.ArgumentParser(description="CLI tool example with ChainMap")
    parser.add_argument("--output",  type=str,        help="Output file name")
    parser.add_argument("--verbose", action="store_true", help="Enable verbose mode")
    parser.add_argument("--mode",    type=str,        help="Set the mode of operation")

    # Simulation d'arguments en ligne de commande
    args = parser.parse_args(["--output", "output_from_cli.log", "--verbose"])

    default_settings = {
        "output":  "default.log",
        "verbose": False,
        "mode":    "normal"
    }

    # Conversion de l'espace de noms args en dictionnaire (sans les None)
    cli_arguments = {k: v for k, v in vars(args).items() if v is not None}
    settings = ChainMap(cli_arguments, default_settings)

    print("Output file:", settings["output"])
    print("Verbose mode:", ("enabled" if settings["verbose"] else "disabled"))
    print("Mode:", settings["mode"])

if __name__ == "__main__":
    main()

Scope resolution in interpreters

from collections import ChainMap

global_scope = {"x": 2, "y": 3}
local_scope  = {"y": 5}

# La portée locale a la priorité sur la portée globale
current_env = ChainMap(local_scope, global_scope)

print("x:", current_env["x"])  # Depuis global_scope
print("y:", current_env["y"])  # La portée locale surpasse le global

# Ajout d'une variable dans la portée globale
current_env.parents["z"] = 100
print("z:", current_env["z"])

print(current_env)

4.3 deque

4.3.1 Understanding deque

deque (pronounced “deck”) is short for double-ended queue. This is a generalization of stacks and queues. This collection is implemented as a doubly linked list, where each element contains a reference to the next and previous element. This allows elements to be added and removed from both ends in O(1).

Creating a deque

from collections import deque

# Deque vide
dq = deque()
print(dq)

# Depuis un tuple
dq = deque((1, 2, 3))
print(dq)

# Depuis une liste
dq = deque([1, 2, 3])
print(dq)

# Depuis un dictionnaire view
dict1 = {"a": 1, "b": 2, "c": 3}
dq = deque(dict1.items())
print(dq)

Add and delete from both ends

from collections import deque

dq = deque([1])
dq.append(2)       # Ajout à droite
dq.appendleft(0)   # Ajout à gauche
dq.append(3)       # Ajout à droite
print(dq)

dq.pop()           # Suppression à droite
popped_el = dq.popleft()  # Suppression à gauche
print("Popped element from the left:", popped_el)
print(dq)

Performance comparison: deque.appendleft() vs list.insert(0, ...)

import timeit

# Ajout à gauche dans une liste — O(n)
list_time = timeit.timeit(
    'for i in range(10000): lst.insert(0, i)',
    setup='lst = []',
    number=10
)

# Ajout à gauche dans un deque — O(1)
deque_time = timeit.timeit(
    'for i in range(10000): deq.appendleft(i)',
    setup='from collections import deque; deq = deque()',
    number=10
)

print(f"List left-append time: {list_time}")
print(f"Deque left-append time: {deque_time}")

Accessing, deleting and inserting elements

from collections import deque

numbers = deque([1, 2, 3, 4, 5, 6])

# Accès par index — O(n)
print("numbers[5]:", numbers[5])

# Suppression avec del
del numbers[5]

# Insertion à une position spécifique
numbers.insert(1, 10)

# Suppression d'un élément par valeur
numbers.remove(5)

print(numbers)

bounded deque

from collections import deque

numbers = deque([0, 1, 2, 3], maxlen=5)
print("Maxlen:", numbers.maxlen)
print(numbers)

numbers.appendleft(-1)
print("After numbers.appendleft(-1):\t", numbers)  # [-1, 0, 1, 2, 3]

numbers.append(4)   # Discarde -1
print("After numbers.append(4):\t", numbers)        # [0, 1, 2, 3, 4]

numbers.append(5)   # Discarde 0
print("After numbers.append(5):\t", numbers)        # [1, 2, 3, 4, 5]

numbers.appendleft(0)  # Discarde 5
print("After numbers.appendleft(0):\t", numbers)    # [0, 1, 2, 3, 4]

Special deque methods

from collections import deque

letters = deque(["a", "b", "c"])
print(letters)

# Rotation d'un pas vers la droite
letters.rotate()
print("letters.rotate():\t", letters)    # deque(['c', 'a', 'b'])

# Rotation de deux pas vers la droite
letters.rotate(2)
print("letters.rotate(2):\t", letters)  # deque(['a', 'b', 'c'])

# Rotation d'un pas vers la gauche
letters.rotate(-1)
print("letters.rotate(-1):\t", letters) # deque(['b', 'c', 'a'])

# Ajout de plusieurs éléments à droite
letters.extend(["d", "e"])
# Ajout de plusieurs éléments à gauche
letters.extendleft(["x", "y"])
print(letters)

4.3.2 Use cases of deque

Maintaining a list of recent items

from collections import deque

recent_items = deque(maxlen=3)
for i in range(5):
    recent_items.append(i)
    print(f"Item {i} added, recent items: {list(recent_items)}")

Implementing a queue (FIFO)

from collections import deque
import time

def process_task(task):
    print("Processing task:", task)
    time.sleep(0.5)

task_queue = deque()

# Simulation d'ajout de tâches
for i in range(1, 5):
    task_queue.append(f"task_{i}")

# Traitement des tâches (FIFO — First In, First Out)
while task_queue:
    current_task = task_queue.popleft()
    process_task(current_task)

Implementing a stack (LIFO)

from collections import deque

class BrowserHistory:
    def __init__(self):
        self.pages = deque(maxlen=5)

    def visit_page(self, page_url):
        """Visit a new page and add it to the history."""
        self.pages.append(page_url)
        print("Visiting:", page_url)

    def go_back(self):
        """Go back to the previous page."""
        if self.pages:
            current_page = self.pages.pop()
            print("Going back from:", current_page)
            if self.pages:
                print("Current page is now:", self.pages[-1])
            else:
                print("No more pages in history.")
        else:
            print("No pages in history.")

browser_history = BrowserHistory()
browser_history.visit_page("home.html")
browser_history.visit_page("about.html")
browser_history.visit_page("contact.html")

browser_history.go_back()  # Retour de contact.html
browser_history.go_back()  # Retour de about.html
browser_history.go_back()  # Plus de pages

Schedule rotation

from collections import deque

schedule = deque(["Alice", "Bob", "Charlie"])

for week in range(1, 5):
    print(f"Week {week} schedule: {list(schedule)}")
    schedule.rotate(1)

Processing from both ends

from collections import deque

class RestaurantWaitlist:
    def __init__(self):
        self.waitlist = deque()

    def arrive(self, name, vip=False):
        if vip:
            self.waitlist.appendleft(name)
            print(f"VIP customer {name} added to the front of the waitlist.")
        else:
            self.waitlist.append(name)
            print(f"Customer {name} added to the end of the waitlist.")

    def seat_customer(self):
        if self.waitlist:
            name = self.waitlist.popleft()
            print(f"Customer {name} is now being seated.")
        else:
            print("The waitlist is currently empty.")

waitlist = RestaurantWaitlist()
waitlist.arrive("A")
waitlist.arrive("B")
waitlist.arrive("C", vip=True)  # Client VIP — passe en premier
waitlist.arrive("D")

waitlist.seat_customer()  # C en premier (VIP)
waitlist.seat_customer()  # Puis A (FIFO)

Implementation of a sliding window

from collections import deque

def moving_average(temperatures, n=5):
    it = iter(temperatures)
    window = deque(maxlen=n)
    for temperature in it:
        window.append(temperature)
        if len(window) == n:
            yield sum(window) / n

temperatures = [22, 21, 23, 26, 24, 25, 27, 28, 22, 19, 20, 18]
print("5-hour Moving Average:", list(moving_average(temperatures)))

4.4 Summary of Module 4

  • namedtuple(): factory function returning a tuple subclass with named fields. Combines the immutability of tuples with the readability of attribute access. Offers better performance and a reduced memory footprint compared to data classes and dictionaries.
  • ChainMap: groups several dictionaries into a single view. Particularly useful for applications requiring multiple dictionaries representing scopes or contexts, such as in templating languages ​​or variable resolution.
  • deque: thread-safe and memory-efficient double-sided queue, allowing addition and deletion from both ends in O(1). Ideal for implementing queues (FIFO) and stacks (LIFO).

5. Customizing built in data types

The collections module provides three classes — UserString, UserList and UserDict — which are wrappers around the built-in types str, list and dict. Their main purpose was to allow subclassing of built-in Python types before Python 2.2. Since this version, it is possible to directly subclass built-in types.

These classes are all implemented in pure Python, making them slower than native types implemented in C.

5.1 UserString

UserString is a wrapper around the built-in str type. The content is stored in a data attribute which is a real instance of str.

Extend str functionality

from collections import UserString

class PalindromeString(UserString):
    def is_palindrome(self):
        cleaned = ''.join(filter(str.isalnum, self.data.lower()))
        return cleaned == cleaned[::-1]

class StrPalindromeString(str):
    def is_palindrome(self):
        cleaned = ''.join(filter(str.isalnum, self.lower()))
        return cleaned == cleaned[::-1]

str1 = PalindromeString("A man, a plan, a canal, Panama")
str2 = StrPalindromeString("A man, a plan, a canal, Panama")
print(str1.is_palindrome())  # True
print(str2.is_palindrome())  # True

Change str functionality

from collections import UserString

# Rendre les opérateurs de comparaison insensibles à la casse
class CIString(UserString):
    def __eq__(self, other):
        return self.data.lower() == other.lower()

    def __lt__(self, other):
        return self.data.lower() < other.lower()

    def __gt__(self, other):
        return self.data.lower() > other.lower()

str1 = CIString("ABCD")
str2 = "abCD"
str3 = "Abcd"

print(str1 == str2)  # True
print(str2 == str3)  # True (avec CIString, sinon False)

This modification also works with the str class directly.

When UserString is preferable to str

UserString is particularly useful when we need to modify the behavior during initialization. The reason: str is an immutable class implemented in C, and its creation is handled by __new__() rather than __init__().

from collections import UserString
from urllib.parse import quote_plus

class URLEncodedString(UserString):
    def __init__(self, string):
        encoded = quote_plus(string)
        super().__init__(encoded)

book_title = "Moby-Dick; or, The Whale"
encoded_title = URLEncodedString(book_title)
search_url = f"https://example.com/search?q={encoded_title}"
print(search_url)

The same implementation with str directly would produce an error, because str.__init__() is not the correct place to modify the value.


5.2 UserList

UserList is a wrapper around the built-in list type. Like UserString, its content is stored in a data attribute.

Subclassing of list vs UserList

# Implémentation avec list
class UniqueList(list):
    def __init__(self, iterable=[]):
        super().__init__()
        self.extend(iterable)

    def append(self, item):
        if item not in self:
            super().append(item)

    def extend(self, iterable):
        for item in iterable:
            self.append(item)

unique_list = UniqueList([1, 2, 2, 3, 4, 4, 4])
print(unique_list)

unique_list.append(5)
unique_list.append(5)  # Doublon ignoré
print(unique_list)
unique_list.extend([6, 6, 7])
print(unique_list)
# Implémentation avec UserList
from collections import UserList

class UniqueList(UserList):
    def __init__(self, iterable=[]):
        super().__init__()
        self.extend(iterable)

    def append(self, item):
        if item not in self.data:  # Utiliser self.data
            super().append(item)

    def extend(self, iterable):
        for item in iterable:
            self.append(item)

unique_list = UniqueList([1, 2, 2, 3, 4, 4, 4])
print(unique_list)

unique_list.append(5)
unique_list.append(5)
print(unique_list)
unique_list.extend([6, 6, 7])
print(unique_list)

The only difference is the use of self.data instead of self. In most cases, UserList offers no real advantage over directly subclassing list, other than backwards compatibility.


5.3 UserDict

UserDict is the only one of the three wrappers that offers any real practical utility over direct subclassing of the built-in type.

The reason: the dict class is implemented in C, and its __init__() and update() methods do not use __setitem__() internally. As a result, overriding __setitem__() in a subclass of dict does not capture all updates. UserDict, on the other hand, is implemented in pure Python and uses __setitem__() for all update operations.

Comparison: subclassing of UserDict vs dict

# Avec UserDict — comportement attendu
from collections import UserDict

class StringDict(UserDict):
    def __setitem__(self, key, value):
        if not isinstance(value, str):
            raise TypeError(f"Value must be of type string")
        super().__setitem__(key, value)

d = StringDict({"a": "1"})
d["b"] = "2"
d.update({"c": "3"})
print(d)
# Avec dict — comportement inattendu
class StringDict(dict):
    def __setitem__(self, key, value):
        if not isinstance(value, str):
            raise TypeError(f"Value must be of type string")
        super().__setitem__(key, value)

d = StringDict({"a": 1})   # Pas d'erreur ! __init__ n'appelle pas __setitem__
d["b"] = "2"               # Pas d'erreur
d.update({"c": 3})         # Pas d'erreur ! update() n'appelle pas __setitem__
print(d)

With dict, to obtain the same behavior, it would also be necessary to override __init__() and update().

Conclusion on UserDict: If you need to create a simple dictionary class that overloads the way updates are performed, and performance is not a priority, UserDict is the best choice.


5.4 Summary of Module 5

The UserString, UserList and UserDict classes of the collections module allow you to override the behavior of built-in Python types. These are all wrappers around native classes, implemented in pure Python (therefore slower).

ClassMain advantageRecommendation
UserStringAllows you to modify the behavior during initialization (__init__)Useful when str.__new__ is a problem
UserListNo real advantage vs subclassing of listUse for backwards compatibility only
UserDict__setitem__ is used for all update operationsRecommended for simply overriding dictionary updates

6. General conclusion

Python’s collections module extends the capabilities of built-in data types to meet more specific and complex needs. Here is a summary of the classes covered:

ClassDescriptionMain use case
listOrdered and mutable collectionPrototyping, dynamic sequences
dictOrdered and mutable key-value pairsConfiguration, dispatch, memorization
tupleOrdered and immutable sequenceFixed data, multiple return, records
setUnordered collection of unique elementsDeduplication, fast membership tests
defaultdictdict with automatic defaultGrouping, counting, accumulation
OrderedDictdict with order comparisonOrdered sequences, backwards compatibility
Counterfrequency counting dictNLP, data analysis, inventories
namedtuple()Tuple with named fieldsReadable immutable records, replacing simple tuples
ChainMapCombined view of several mappingsScope management, configuration with priorities
dequeDouble-sided thread-safe tailFIFO queues, LIFO stacks, sliding windows
UserStringWrap around strCustomizing str with __init__
UserListWrap around listBackward Compatibility
UserDictWrap around dictOwn overload of __setitem__

Mastering these data structures will allow you to write Python code that is more elegant, more efficient and more idiomatic, always choosing the structure best suited to the problem at hand.


Search Terms

python · collections · foundations · data · analysis · engineering · analytics · cases · dictionaries · lists · chainmap · counter · defaultdict · deque · namedtuple · ordereddict · methods · sets · tuples · types

Interested in this course?

Contact us to book it or get a custom training plan for your team.