3 - Data Structures

python
Published

April 27, 2026

In Python, you can check all available built-in method that a certain classes has using dir(). For example below we have a list class and we can check available methods:

l = [3, 2, -1, 6, 12]
dir(l)
['__add__',
 '__class__',
 '__class_getitem__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__iadd__',
 '__imul__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__rmul__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'append',
 'clear',
 'copy',
 'count',
 'extend',
 'index',
 'insert',
 'pop',
 'remove',
 'reverse',
 'sort']
print(l.__len__())
print(len(l))   # short hand
5
5

List

Lists can be created by enclosing elements in square brackets [].

# Creating and manipulating a list of integers
numbers = [1, 2, 3, 4, 5]
print("Original List:", numbers)
Original List: [1, 2, 3, 4, 5]

Lists are mutable, meaning their elements can be modified after the list is created using their index.

# Lists are mutable: elements can be changed
numbers[0] = 10  # Changing the first element
print(f"List after changing first element: {numbers}")
List after changing first element: [10, 2, 3, 4, 5]

You can access elements using their zero-based index.

# Accessing elements using their position
second_element = numbers[1]
print(f"second_element: {second_element}")
second_element: 2

Slicing allows you to retrieve a sublist using the syntax [start:stop:step].

# Slicing
first_three = numbers[:3]
print(f"First three elements: {first_three}")

# Slicing from and to a specific position
middle_three = numbers[1:4]
print(f"Middle three elements: {middle_three}")

# Slicing with a step
every_other = numbers[::2]
print(f"Every other element: {every_other}")
First three elements: [10, 2, 3]
Middle three elements: [2, 3, 4]
Every other element: [10, 3, 5]

A common trick to reverse a list is to use a negative step during slicing.

# Reversing a list
reversed_list = numbers[::-1]
print(f"Reversed List: {reversed_list}")
Reversed List: [5, 4, 3, 2, 10]

Negative indexing is highly useful for accessing elements relative to the end of the list.

# Accessing the last element
last_element = numbers[-1]
print(f"Last element: {last_element}")
Last element: 5

Python automatically resizes lists as elements are added or removed, managing memory for you dynamically.

# Discussing memory management
# Python lists automatically resize as items are added or removed
numbers.append(6)
print(f"List after appending an element: {numbers}")
List after appending an element: [10, 2, 3, 4, 5, 6]

Unlike arrays in many other programming languages, Python lists can contain elements of mixed data types.

# Lists can contain different types of objects
mixed_list = [1, "Hello", 3.14, [2, 4, 6]]
print("Mixed Type List:", mixed_list)
Mixed Type List: [1, 'Hello', 3.14, [2, 4, 6]]
Note

A list is suboptimal for frequent searches. Consider an array: while typically fixed in size, it offers faster access since elements are stored in a contiguous memory block.

Array

import numpy as np

1a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

2c = np.append(a, 4)

print(f"a: {a}, c: {c}")
print(f"id: a -> {id(a)}, id: c -> {id(c)}")

3print(f"a + b = {a + b}")
print(f"a * b = {a * b}")

data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

4print(f"Mean: {np.mean(data)}")
print(f"median: {np.median(data)}")
print(f"std: {np.std(data)}")
1
Arrays are created using numpy. They are highly optimized for numerical operations compared to standard Python lists.
2
In contrast to list, appending to a numpy array actually allocates a brand new continuous block of memory and creates a new array (notice their different memory ids when printed).
3
Array mathematical operations are natively vectorized. For example, a + b performs element-wise addition implicitly without needing a for loop.
4
numpy provides numerous built-in summary statistics methods that can be computed across arrays efficiently.
a: [1 2 3], c: [1 2 3 4]
id: a -> 4635285456, id: c -> 4635285840
a + b = [5 7 9]
a * b = [ 4 10 18]
Mean: 5.5
median: 5.5
std: 2.8722813232690143

Dictionary

Each key in a Python dictionary has a harsh value that makes it extremly efficient for lookup using keys because a harsh value directly correspond to memory address, which makes searching for an item in a dictionary fast (in fact constant time) \((O(1))\). The drawback is that it requires more memory because they need to store the keys.

Basic operations

def main() -> None:
    # Creating and manipulating a dictionary
    person: dict[str | int, str] = {
        "name": "Arjan",
        "profession": "Developer",
        "city": "Utrecht",
    }
    print("Original Dictionary:", person)

    # Dictionaries are mutable: values can be changed based on their keys
    person["name"] = "Jane"  # Changing the value associated with the key 'name'
    print(f"Dictionary after changing 'name': {person}")

    # Accessing elements using their keys
    profession = person["profession"]
    print(f"profession: {profession}")

    # Adding a new key-value pair
    person["profession"] = "YouTuber"
    print(f"Dictionary after adding a new key-value pair: {person}")

    # Removing a key-value pair
    del person["city"]
    print(f"Dictionary after removing 'city': {person}")

    # Keys and values can be of different types
    person[1] = "One"
    print("Mixed Type Dictionary:", person)

    # Discussing memory management
    # Python dictionaries automatically resize and rehash as items are added or removed
    person["hobby"] = "Photography"
    print(f"Dictionary after adding a new hobby: {person}")

    # Getting a list of all keys and values
    print(f"Keys: {person.keys()}")
    print(f"Values: {person.values()}")


if __name__ == "__main__":
    main()
Original Dictionary: {'name': 'Arjan', 'profession': 'Developer', 'city': 'Utrecht'}
Dictionary after changing 'name': {'name': 'Jane', 'profession': 'Developer', 'city': 'Utrecht'}
profession: Developer
Dictionary after adding a new key-value pair: {'name': 'Jane', 'profession': 'YouTuber', 'city': 'Utrecht'}
Dictionary after removing 'city': {'name': 'Jane', 'profession': 'YouTuber'}
Mixed Type Dictionary: {'name': 'Jane', 'profession': 'YouTuber', 1: 'One'}
Dictionary after adding a new hobby: {'name': 'Jane', 'profession': 'YouTuber', 1: 'One', 'hobby': 'Photography'}
Keys: dict_keys(['name', 'profession', 1, 'hobby'])
Values: dict_values(['Jane', 'YouTuber', 'One', 'Photography'])

Enums

from enum import IntEnum, StrEnum, auto


class HTTPStatus(IntEnum):
    OK = 200
    CREATED = 201
    ACCEPTED = 202
    NO_CONTENT = 204
    BAD_REQUEST = 400
    UNAUTHORIZED = 401
    FORBIDDEN = 403
    NOT_FOUND = 404
    INTERNAL_SERVER_ERROR = 500
    NOT_IMPLEMENTED = 501

class RESTMethod(StrEnum):
    GET = auto()
    POST = auto()
    PUT = auto()
    DELETE = auto()

for status in HTTPStatus:
    print(f"{status.name}: {status.value}")


print(f"Name of the enum member: {HTTPStatus.OK.name}")

print(f"Enum member from value: {HTTPStatus(404)}")

print(f"value of the enum member: {HTTPStatus.NOT_FOUND.value}")


def response_description(status: HTTPStatus) -> str:
    if status == HTTPStatus.OK:
        return "Request succeeded"
    elif status == HTTPStatus.NOT_FOUND:
        return "Resource not found"
    else:
        return "Error occurred"


def main() -> None:
    description = response_description(HTTPStatus.OK)
    print(description)

    if HTTPStatus.BAD_REQUEST == HTTPStatus.BAD_REQUEST:
        print("Both are client error responses")

    if HTTPStatus.INTERNAL_SERVER_ERROR in HTTPStatus:
        print("500 Internal Server Error is a valid HTTP status code.")


if __name__ == "__main__":
    main()
OK: 200
CREATED: 201
ACCEPTED: 202
NO_CONTENT: 204
BAD_REQUEST: 400
UNAUTHORIZED: 401
FORBIDDEN: 403
NOT_FOUND: 404
INTERNAL_SERVER_ERROR: 500
NOT_IMPLEMENTED: 501
Name of the enum member: OK
Enum member from value: 404
value of the enum member: 404
Request succeeded
Both are client error responses
500 Internal Server Error is a valid HTTP status code.

Tuples

  • Immutability: once you create a value in a tuple, you can not alter it.
from enum import StrEnum
from typing import Union

# Grouping several values
coordinates = (10.0, 20.5)
print(coordinates)


class Month(StrEnum):
    JANUARY = "January"
    FEBRUARY = "February"


def get_birthday() -> tuple[Month, int]:
    # Example function returning a tuple of month (StrEnum) and year (integer)
    return (Month.JANUARY, 1990)


result = get_birthday()
print(result)


hash = hash(result)  # Tuples are hashable

my_dict = {hash: result}
print(my_dict)

# Simple combination of a few variables
person_info = ("John Doe", 30, "Engineer")
print(person_info)

# Tuples typically contain objects of different types
person_details = (
    "Jane Doe",
    Month.FEBRUARY,
    1985,
    True,
)  # Name, birth month, birth year, resident
print(person_details)

# Access by index, order matters
name, age, profession = person_info
print(f"Name: {name}, Age: {age}, Profession: {profession}")

month, year = get_birthday()
print(f"Month: {month}, Year: {year}")


# Tuples are not ideal for ordered collections of the same type
numbers = (1, 2, 3)  # A list would be more suitable for this purpose


# Define a command as a tuple with various possible types
Command = Union[tuple[str, int], tuple[str, int, int], tuple[str]]


def handle_command(command: Command):
    match command:
        case ("add", x, y):
            print(f"Adding {x} and {y}: {x + y}")
        case ("increment", x):
            print(f"Incrementing {x}: {x + 1}")
        case ("reset",):
            print("Resetting to zero")
        case _:
            print("Unknown command")


def main() -> None:
    handle_command(("add", 1, 2))  # Output: Adding 1 and 2: 3
    handle_command(("increment", 10))  # Output: Incrementing 10: 11
    handle_command(("reset",))  # Output: Resetting to zero
    handle_command(("unknown", 123))  # Output: Unknown command


if __name__ == "__main__":
    main()
(10.0, 20.5)
(<Month.JANUARY: 'January'>, 1990)
{2464651493728378369: (<Month.JANUARY: 'January'>, 1990)}
('John Doe', 30, 'Engineer')
('Jane Doe', <Month.FEBRUARY: 'February'>, 1985, True)
Name: John Doe, Age: 30, Profession: Engineer
Month: January, Year: 1990
Adding 1 and 2: 3
Incrementing 10: 11
Resetting to zero
Unknown command