The Honest Code Manifesto

You cleared the beginner threshold. Your code runs.

Congratulations. You just reached the level of functional mediocrity.

Your programs execute, but don’t kid yourself, that is not an achievement. It is the minimum requirement. It is the equivalent of a carpenter taking pride in the fact that his chair doesn’t collapse when you sit on it. Nobody throws a party for that, it is simply what was expected.

You look at your work and feel a void. You read a master’s code and it isn’t merely different. It is solid. It has intent. Every line serves a clear purpose. Your code, meanwhile, is the work of a bricklayer, not an architect.

I am not going to teach you tricks to impress your colleagues in a code review. Don’t be sad.

I am going to teach you 15 laws.

Non-negotiable principles for dropping the amateur’s habits and adopting the craftsman’s discipline. Each law is designed to destroy a weakness in the way you think and replace it with rigour.

Don’t expect a comfortable read. Every chapter will force you to question your habits, to unlearn what you thought you knew. The goal is not to add more knowledge to the pile, it is to purge the weakness from your logic.

The result won’t be “prettier” code. It will be honest code.

Code that doesn’t hide its intent behind clumsy loops or confusing variable names. Code that holds up under the pressure of change. Code built with honour.

Code that, at last, you won’t have to apologise for.

Section 1: Pythonic Fundamentals

Welcome, initiate.

You got here because you know there is something more. You write code, and it works. Congratulations.

A hammer also works for opening a nut, but the result is a mess. The difference between a craftsman and an amateur is not in the what, it is in the how.

These first two tricks are your initiation. They are the fundamental katas. Burn them into your memory, feel them in your fingers. They are the first step towards no longer being a mere scribe of code and becoming a master of the language.

Trick #1: List Comprehensions, the Way of the Single Fist

All your life you have been taught to build things step by step. Brick by brick. And to create a list out of another list, you have written the same ritual over and over.

The Heresy: The Ritual of the Five Prayers

You want the squares of a list of numbers. Your instinct, tamed by habit, takes you here:

# A clumsy, predictable ritual.
numeros_originales = [1, 2, 3, 4, 5, 6]
cuadrados = []  # 1. You start empty-handed.

for numero in numeros_originales:  # 2. You begin the procession.
    cuadrado = numero * numero  # 3. You do an obvious calculation.
    cuadrados.append(cuadrado)  # 4. You humbly append the result.

# 5. Finally, you admire your work.
print(cuadrados)
# Output: [1, 4, 9, 16, 25, 36]

Five lines. Five steps for an idea your brain processes in an instant. That is cowardly code. It is afraid of being direct. It hides behind ceremony.

The Master’s Path: One Strike, One Result

The master does not hesitate. His intent and his action are one and the same. He does not “build” the list, he declares it.

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

cuadrados = [numero * numero for numero in numeros_originales]

print(cuadrados)
# Output: [1, 4, 9, 16, 25, 36]

Look at it. The essence of the operation lives in a single line of power. [EXPRESSION for ELEMENT in LIST]. No intermediate steps. No hesitation. It is the direct manifestation of your will.

The Revelation: Why This Makes You Better

  • Clarity of intent: the code shouts what it does. “I want a list of numero * numero”. The traditional for loop whispers: “I am going to start a process that, if all goes well, will end up giving me a list of squares”. Direct intent always beats hesitation.
  • Less noise, more signal: you remove the noise of initialising an empty list and calling .append(). Every line of code is an opportunity for a bug. Fewer lines, fewer places for bugs to hide.
  • Efficiency forged in C: this is not only about beauty. List comprehensions are often faster than manual for loops. They are optimised at a deeper level of the interpreter. Your code doesn’t just look better, it runs better and faster.

Bend Reality: The Conditional Strike

But what if you only want the squares of the even numbers? The novice would add an if inside the loop, piling more ceremony onto an already clumsy ritual.

The master folds the condition into the same strike.

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

# Add the condition at the end. Simple. Lethal.
cuadrados_pares = [n**2 for n in numeros_originales if n % 2 == 0]

print(cuadrados_pares)
# Output: [4, 16, 36]

The structure is [EXPRESSION for ELEMENT in LIST if CONDITION]. Brand it into your brain. Next time your fingers start typing mi_lista = [] followed by a for, stop. Breathe. And land a single clean strike.

Trick #2: Dictionary and Set Comprehensions, Forging Structures by Will

You have tasted the power. You have seen that a list can be born out of pure intent. But the initiate stops there. The master knows this power has no limits.

The Heresy: Manual Assembly

Imagine you want a dictionary with numbers as keys and their squares as values. The beginner’s path is, once again, manual and tedious labour.

numeros = [1, 2, 3, 4]
cuadrados_dict = {} # The blank canvas, again.

for n in numeros:
    cuadrados_dict[n] = n * n # Manual assignment, one by one.

print(cuadrados_dict)
# Output: {1: 1, 2: 4, 3: 9, 4: 16}

And for a set with the unique letters of a word:

palabra = "disciplina"
letras_unicas = set() # An empty sack.

for letra in palabra:
    letras_unicas.add(letra) # Adding them one by one, like a peasant.

print(letras_unicas)
# Output: {'d', 'p', 'i', 'c', 'l', 's', 'n', 'a'}

This code has no soul. It is the work of an accountant, not a creator.

The Master’s Path: Summon the Structure

The master does not “fill” a dictionary or a set. He summons it into existence, fully formed.

numeros = [1, 2, 3, 4]

# The syntax is almost identical. The power is the same.
# {KEY: VALUE for ELEMENT in LIST}
cuadrados_dict = {n: n*n for n in numeros}
print(cuadrados_dict)
# Output: {1: 1, 2: 4, 3: 9, 4: 16}

# For the set it is even simpler.
# {EXPRESSION for ELEMENT in LIST}
palabra = "disciplina"
letras_unicas = {letra for letra in palabra}
print(letras_unicas)
# Output: {'d', 'p', 'i', 'c', 'l', 's', 'n', 'a'}

The braces {} tell Python your intent: forge a dictionary or a set. The key: value syntax marks the dictionary. The absence of a colon creates the set. It is a language of power, subtle and precise.

The Revelation: Declaring vs. Building

The difference is philosophical. The heretic builds the data structure step by step. He is stuck inside the process. The master declares the final data structure. He is focused on the result.

When you read {n: n*n for n in numeros}, your mind sees a complete dictionary right away. You don’t have to mentally simulate a loop, an assignment, another iteration, another assignment. The code expresses the “what” (a dictionary of squares), not the “how” (a loop that assigns values). That is the pillar of clean code.

Bend Reality: Creating From Existing Dictionaries

This power is not limited to building from lists. You can forge a new dictionary out of another one, filtering and transforming with surgical precision.

stock_precios = {'manzana': 1.50, 'platano': 0.75, 'naranja': 2.25, 'kiwi': 0.99}

# Build a new dictionary with only the expensive fruit (price > 1.0)
frutas_caras = {fruta: precio for fruta, precio in stock_precios.items() if precio > 1.0}

print(frutas_caras)
# Output: {'manzana': 1.5, 'naranja': 2.25}

Note the use of .items() to get key and value at once. Note how the if condition filters elements with the same elegance as in list comprehensions.

You are not writing code. You are sculpting data.

Section 2: Mastery of Data Structures, Forge Your Weapons

A mediocre programmer thinks of data as something you store.

A master understands that data is something you wield.

Data structures (lists, dictionaries, sets) are not simple containers. They are your weapons. Each one has an edge, a weight and a purpose. Using the wrong one is like bringing a spear to a knife fight: clumsy, inefficient and a sign of incompetence.

In this section you will learn to pick the right weapon for each fight. You will stop bludgeoning your data with the generic tool and start applying the precise technique that solves the problem with devastating efficiency and elegance.

Master these tools and your data will bend to your will.

Trick #3: collections.Counter, the Warrior’s Abacus

Counting things.

A task so fundamental, so repetitive, that watching someone do it badly is an insult to the profession.

The Heresy: Manual Counting

You are handed a list of elements and asked to count the occurrences of each one. The novice, with more enthusiasm than brain, builds this:

# Code that screams "I am new at this".
votacion = ['gato', 'perro', 'gato', 'gato', 'pez', 'perro', 'hamster', 'gato']
conteo = {}

for animal in votacion:
    if animal not in conteo:
        conteo[animal] = 1
    else:
        conteo[animal] += 1

print(conteo)
# Output: {'gato': 4, 'perro': 2, 'pez': 1, 'hamster': 1}

This code is a swamp of conditional logic. The if/else muddies the main intent. You are managing the counting process instead of simply counting. It is the equivalent of building your own hammer every time you want to drive a nail.

The Master’s Path: The Strike That Counts

The master does not manage. He commands. And for counting, there is a specific command.

from collections import Counter

votacion = ['gato', 'perro', 'gato', 'gato', 'pez', 'perro', 'hamster', 'gato']

conteo = Counter(votacion)

print(conteo)
# Output: Counter({'gato': 4, 'perro': 2, 'pez': 1, 'hamster': 1})

One line. One intent. The code does not say “I am going to iterate and count”. It says “this is a count of that”. Counter is a dictionary subclass built for a single mission. It is a specialist, and the specialist always beats the generalist.

The Revelation: Delegate and You Will Win

Your job is not to reinvent the basic tools. Your job is to solve hard problems. By using Counter you delegate the trivial, repetitive task of counting to a tool optimised for it.

  • Absolute readability: Counter(votacion) explains itself. The manual loop requires mental analysis.
  • Extra functionality: a Counter object comes with superpowers. It is not a plain dictionary. For instance, you can ask for the most common elements with insulting ease.

Bend Reality: Most Wanted and Set Arithmetic

Want to know the two most voted animals? The heretic would have to sort the dictionary by values, a clumsy ritual. The master simply asks:

# Get the 2 most common elements.
print(conteo.most_common(2))
# Output: [('gato', 4), ('perro', 2)]

You can also do arithmetic with them. Imagine a second round of voting.

votacion_2 = ['perro', 'perro', 'gato', 'pez', 'pez', 'pez']
conteo_2 = Counter(votacion_2)

# Combine the counts
conteo_total = conteo + conteo_2
print(conteo_total)
# Output: Counter({'gato': 5, 'pez': 4, 'perro': 4, 'hamster': 1})

Using a manual dictionary for this is a recipe for garbage code. Use Counter. Show that you respect your time and everyone else’s.

Trick #4: collections.defaultdict, the Dictionary That Anticipates Your Wishes

KeyError is the plague of dictionaries. It is proof that your code is fragile, that it doesn’t anticipate the unknown. The defensive code written to avoid it is often worse than the error itself.

The Heresy: The Wall of ifs

You want to group a list of words by their first letter. The beginner’s approach is defensive, fearful.

palabras = ['gato', 'perro', 'gallina', 'pez', 'gorila', 'paloma']
agrupadas = {}

for palabra in palabras:
    primera_letra = palabra[0]
    if primera_letra not in agrupadas:
        # Create the entry for the first time, fearfully.
        agrupadas[primera_letra] = []

    # Append the word, now that it is safe.
    agrupadas[primera_letra].append(palabra)

print(agrupadas)
# Output: {'g': ['gato', 'gallina', 'gorila'], 'p': ['perro', 'pez', 'paloma']}

That if block is a monument to insecurity. Every time you touch the dictionary you have to ask first: “May I? Does the key already exist?”. This code stutters.

The Master’s Path: The Invisible Safety Net

The master does not ask. He acts. And he uses a tool that turns the failure into an opportunity.

from collections import defaultdict

palabras = ['gato', 'perro', 'gallina', 'pez', 'gorila', 'paloma']
# A dictionary that, if a key is missing, creates it by calling list().
agrupadas = defaultdict(list)

for palabra in palabras:
    # No `if`. No fear. Just act.
    agrupadas[palabra[0]].append(palabra)

print(agrupadas)
# Output: defaultdict(<class 'list'>, {'g': ['gato', 'gallina', 'gorila'], 'p': ['perro', 'pez', 'paloma']})

defaultdict(list) creates a dictionary with one rule: if you try to access a key that doesn’t exist, create it automatically and assign as its value the result of calling list(), that is, an empty list [].

The Revelation: Write Optimistic Code

The heretic’s code is pessimistic: it assumes the key isn’t there and always checks.

The master’s code is optimistic: it assumes the key exists (or will be created on the fly) and acts directly.

That removes an entire layer of conditional logic. Your code focuses on the action (append(palabra)) instead of the preparation (if key not in...). The train of thought never breaks. The result is cleaner, shorter and dramatically more readable code.

Bend Reality: Beyond Lists

The power of defaultdict is not limited to list. You can hand it any default value “factory”.

  • defaultdict(int): perfect for counting. If the key doesn’t exist, it is created with a value of 0.

      conteo_letras = defaultdict(int)
      for letra in "abracadabra":
          conteo_letras[letra] += 1
      # Output: defaultdict(<class 'int'>, {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
    
  • defaultdict(set): for grouping unique elements.
  • defaultdict(lambda: "desconocido"): use a lambda for custom default values.

defaultdict is not just a convenience. It is a philosophy. It is the decision to write code that flows instead of code that trips.

Trick #5: zip, the Art of the Synchronised March

You have two (or more) related lists. Names and ages. Products and prices. And you need to process them in parallel.

The Heresy: The Clumsy Index

The untrained programmer reaches for the only tool he knows: the numeric index. And he forges an abomination.

nombres = ['Ana', 'Luis', 'Eva']
edades = [28, 34, 41]
# DO NOT DO THIS!
for i in range(len(nombres)):
    nombre = nombres[i]
    edad = edades[i]
    print(f"{nombre} tiene {edad} años.")

This code is an accident waiting to happen.

  1. It is ugly: nombres[i] and edades[i] are visual noise.
  2. It is fragile: what happens if edades has one element fewer than nombres? The code blows up with an IndexError.
  3. It is not Pythonic: Python hands you elegant tools to avoid this kind of manual bookkeeping. Ignoring them is disrespectful.

The Master’s Path: The Direct Link

The master does not worry about indexes. He worries about correspondences. And for that, he uses zip.

nombres = ['Ana', 'Luis', 'Eva']
edades = [28, 34, 41]

for nombre, edad in zip(nombres, edades):
    print(f"{nombre} tiene {edad} años.")

zip takes two or more iterables and “zips” them together, producing tuples with the matching elements of each. It is direct, clean and safe.

The Revelation: Abstracting the Mechanism

zip abstracts away the mechanism of parallel iteration. You no longer worry about counters, ranges or indexes. You focus on the result: pairs of related elements.

  • Safety: zip stops as soon as the shortest iterable runs out. No IndexError. It is safe by construction.
  • Scalability: need a third list? Just add it to the zip call.

      ciudades = ['Madrid', 'Lima', 'México DF']
      for nombre, edad, ciudad in zip(nombres, edades, ciudades):
          print(f"{nombre} ({edad}) vive en {ciudad}.")
    

    Doing this with manual indexes hurts even more.

Bend Reality: Unzipping

The beauty of zip is symmetric. If you can zip, you can also unzip.

pares = [('Ana', 28), ('Luis', 34), ('Eva', 41)]

# The asterisk (*) unpacks the pairs into zip
nombres, edades = zip(*pares)

print(nombres)  # Output: ('Ana', 'Luis', 'Eva')
print(edades)   # Output: (28, 34, 41)

This is a high-level idiom. With zip(*...) you are saying:

“Take this list of pairs and rotate them into two separate sequences”.

It is a matrix transposition, expressed with startling clarity and brevity.

Stop using indexes to iterate. That is cave-dweller language. Use zip and speak like a master.

Section 3: High-Impact Functions, the Engine of Your Logic

Functions are the verbs of your code. They are the units of action.

A novice writes functions that behave like cheap single-use tools: rigid, fragile and limited. They do one thing and nothing else. The master forges functions that behave like precision engines: flexible, powerful and reusable.

In this section you will stop writing simple code recipes. You will learn to build logical mechanisms that adapt, that expand and that can be combined to create complex systems. Understanding these techniques is the difference between putting up a wooden shack and designing a skyscraper. Both give you shelter, but only one proves mastery.

Trick #6: Flexible Arguments With *args and **kwargs, the Net That Catches Everything

Your function needs to add numbers. But how many? Two, three, twenty? The initiate commits the sin of rigidity.

The Heresy: Several Functions for a Single Idea

The amateur’s approach is naive and repetitive. Either he creates a function for every case, or he expects the data to arrive in a clumsy list.

# A rigid, embarrassing design.
def sumar_dos(a, b):
    return a + b

def sumar_tres(a, b, c):
    return a + b + c

# Or this other abomination:
def sumar_lista(numeros):
    total = 0
    for n in numeros:
        total += n
    return total

sumar_lista([1, 2, 3, 4])

This code forces you to package your data before passing it. An extra step, a nuisance that pollutes the call site. It doesn’t flow.

The Master’s Path: The Universal Function

The master writes a function that never asks how many arguments there are. It simply accepts them.

def sumador_universal(*args):
    print(f"He recibido estos argumentos posicionales: {args}")
    total = 0
    for n in args:
        total += n
    return total

print(sumador_universal(1, 2, 3, 4, 5))
# Output:
# He recibido estos argumentos posicionales: (1, 2, 3, 4, 5)
# 15

The asterisk * is the key. It tells Python: “take every positional argument passed to me, from here to the end, and pack them into a tuple called args”.

And for named arguments there is a twin power: **kwargs.

def procesar_datos(**kwargs):
    print(f"He recibido estos argumentos de palabra clave: {kwargs}")
    for clave, valor in kwargs.items():
        print(f"Procesando {clave} = {valor}")

procesar_datos(nombre="Arturo", id=112, ciudad="Madrid", status="Activo")
# Output:
# He recibido estos argumentos de palabra clave: {'nombre': 'Arturo', 'id': 112, 'ciudad': 'Madrid', 'status': 'Activo'}
# Procesando nombre = Arturo
# Procesando id = 112
# Procesando ciudad = Madrid
# Procesando status = Activo

The double asterisk ** tells Python: “take every keyword argument (nombre='valor') and pack it into a dictionary called kwargs”.

The Revelation: You Build Adaptable Functions, Not Rigid Ones

  • *args and **kwargs (the names are a convention, you could use *numeros and **opciones) free your functions from the tyranny of a fixed signature.
  • Flexibility: you create APIs and utilities usable in a multitude of situations without rewriting them.
  • Decorators and proxies: they are the backbone of decorators and of any function that needs to forward arguments to another function without knowing them in advance.

Bend Reality: Unpacking in Reverse

The power flows both ways. If * and ** can pack arguments, they can also unpack them when calling a function.

def reporte(nombre, edad, ciudad):
    print(f"INFORME: {nombre}, {edad} años, de {ciudad}.")

# You have the data in a list and a dictionary.
datos_lista = ['Elena', 31, 'Barcelona']
datos_dict = {'nombre': 'Carlos', 'edad': 45, 'ciudad': 'Valencia'}

# The heretic would do this:
reporte(datos_lista[0], datos_lista[1], datos_lista[2])

# The master does this:
reporte(*datos_lista) # Unpacks the list into positional arguments.
reporte(**datos_dict) # Unpacks the dict into keyword arguments.

This technique is sublime. It separates preparing the data from executing the function. It is clean, expressive and the mark of someone who commands the flow of the language.

Trick #7: Functions as First-Class Citizens, Logic as a Throwing Weapon

In lesser languages, functions are second-class entities. They exist, but you cannot treat them like data. They are nailed to their definition. In Python, functions are free.

The Heresy: The if/elif/else Staircase

You need a calculator that can add or subtract.

The novice, thinking rigidly, builds a control structure to decide which code to run.

def calcular(operacion, a, b):
    if operacion == 'sumar':
        return a + b
    elif operacion == 'restar':
        return a - b
    else:
        print("Operación no válida")
        return None

This code is a monolithic block. If you want to add “multiply”, you have to desecrate the calcular function with another elif. The function becomes a landfill of conditional logic. It violates the Open/Closed principle (open for extension, closed for modification).

The Master’s Path: Pass the Action, Not the Order

The master understands that a function is an object, like a number or a string. It can be passed as an argument.

def sumar(a, b):
    return a + b

def restar(a, b):
    return a - b

def calcular(funcion_a_ejecutar, a, b):
    # Just run whatever function you were handed.
    return funcion_a_ejecutar(a, b)

# Pass the sumar function, not the string "sumar".
resultado_suma = calcular(sumar, 10, 5)
resultado_resta = calcular(restar, 10, 5)

print(f"Suma: {resultado_suma}, Resta: {resultado_resta}")
# Output: Suma: 15, Resta: 5

The calcular function has become agnostic. It knows nothing about addition or subtraction. Its only mission is to run whatever logic it is handed. It is a pure shell, an executor of strategies.

The Revelation: Decouple the Invocation From the Implementation

When you treat functions as first-class citizens, you reach a higher level of abstraction.

  • Decoupling: calcular no longer depends on sumar or restar. You can define multiplicar in another file and hand it to calcular without the latter noticing. Your code becomes modular and extensible.
  • Expressive code: you write code that operates on actions, not only on data. That is the doorway into functional programming.

Bend Reality: The Dispatcher

You can push this to the limit and wipe out the if/elif/else structure entirely by using a dictionary where the keys are the orders and the values are the actions.

def multiplicar(a, b):
    return a * b

# A map from strings to functions.
operaciones = {
    'sumar': sumar,
    'restar': restar,
    'multiplicar': multiplicar
}

def despachador(operacion, a, b):
    # Look the function up in the dictionary and run it.
    # Use .get() to handle invalid cases gracefully.
    func = operaciones.get(operacion)
    if func:
        return func(a, b)
    else:
        raise ValueError("Operación no válida")

print(despachador('multiplicar', 6, 7)) # Output: 42

This pattern is infinitely extensible. To add a new operation you only add a new function and a new entry in the dictionary. despachador is never touched. Solid rock.

Section 4: Intelligent Iteration, the Rhythmic Heart of Code

Iteration is the heartbeat of your programs. It is the pulse that pumps data through the logic.

An amateur writes loops that are arrhythmic and clumsy. They eat memory without mercy, they repeat themselves without elegance and they handle control logic with all the subtlety of a hammer. His loops are noise.

The master composes. His loops are rhythmic, efficient and precise. He understands that iterating is not just repeating, it is flowing.

This section will teach you to command that flow. You will learn to chain sequences effortlessly, to handle combinatorics with a single order and to write loops that state their purpose with devastating clarity. Stop making noise. Time to find the rhythm.

Trick #8: itertools.chain, Forging a Single Stream

You have several lists, several sequences, and you need to process them as if they were one. The beginner’s instinct is brute force.

The Heresy: The Memory Retaining Wall

The novice sees two lists and thinks of a bigger one. His first impulse is to join them, creating a new and monstrous object in memory.

# An act of waste and brutality.
heroes = ['Aragorn', 'Gimli', 'Legolas']
villanos = ['Sauron', 'Saruman', 'Ella-Laraña']

# Creates a THIRD list in memory.
todos_personajes = heroes + villanos

for personaje in todos_personajes:
    print(personaje)

# Or worse, repeat the code.
for heroe in heroes:
    print(heroe)
for villano in villanos:
    print(villano)

The first option is a crime against RAM.

If heroes and villanos each held a million elements, you just doubled the memory required for no reason. The second option is a crime against the DRY (Don’t Repeat Yourself) principle. It is lazy code.

The Master’s Path: The Unbroken Flow

The master does not create copies. He creates a view, a logical sequence that links the original iterables without moving a single piece of data.

from itertools import chain

heroes = ['Aragorn', 'Gimli', 'Legolas']
villanos = ['Sauron', 'Saruman', 'Ella-Laraña']

# chain() does not create a new list. It creates a smart iterator.
personajes_enlazados = chain(heroes, villanos)

for personaje in personajes_enlazados:
    print(personaje)

chain forges an iterator that consumes the first iterable until it is exhausted and then, without pausing, starts consuming the second, and so on.

No duplication. No waste.

The Revelation: Iterate Over Views, Not Over Copies

The difference is the same as between reading the table of contents of two books and photocopying both books to staple them together. chain is the table of contents. The + operator is the photocopies.

  • Memory efficiency: the most obvious and critical advantage. You can chain sequences of gigabytes at nearly zero memory cost.
  • Accepts any iterable: chain works with lists, tuples, generators, sets, anything you can iterate over. The + operator is far more restrictive.

Bend Reality: Flattening With chain.from_iterable

Sometimes your sequences live inside another sequence, like a list of lists. The master has a specific kata for this.

equipos = [
    ['Aragorn', 'Gimli', 'Legolas'],
    ['Frodo', 'Sam'],
    ['Saruman', 'Grima']
]

# The heretic would use a nested loop.
# for equipo in equipos:
#     for miembro in equipo:
#         print(miembro)

# The master flattens the structure with a single order.
for miembro in chain.from_iterable(equipos):
    print(miembro)

chain.from_iterable takes an iterable of iterables and turns it into a single stream. It is the definitive weapon for flattening data structures efficiently.

Trick #9: itertools.combinations and permutations, the Dance of Combinatorics

You need to find every possible pair in a group of elements. Or every trio. Or every possible sequence.

The Heresy: The Maze of Nested Loops

Without the right knowledge, the programmer throws himself into building a maze of nested for loops. To find pairs he writes two loops. For trios, three.

# A brave but deeply wrong attempt at finding pairs.
items = ['A', 'B', 'C', 'D']
pares = []
for i in range(len(items)):
    for j in range(i + 1, len(items)):
        pares.append((items[i], items[j]))

print(pares)
# Output: [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]

This code works, but it is a mess. Hard to read, prone to off-by-one index errors and a nightmare to maintain.

And what if you now need trios? Will you add another level of nesting and sink further into madness?

The Master’s Path: Summon the Mathematical Oracle

The master does not write combinatorics algorithms. He summons them from the standard library, where they were forged by experts and optimised in C.

from itertools import combinations, permutations

items = ['A', 'B', 'C', 'D']

# Combinations: order does NOT matter.
pares_combinaciones = list(combinations(items, 2))
print(f"Combinaciones de 2: {pares_combinaciones}")

# Permutations: order DOES matter.
pares_permutaciones = list(permutations(items, 2))
print(f"Permutaciones de 2: {pares_permutaciones}")

Output:

Combinations of 2: [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]

Permutations of 2: [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'A'), ('B', 'C'), ('B', 'D'), ('C', 'A'), ('C', 'B'), ('C', 'D'), ('D', 'A'), ('D', 'B'), ('D', 'C')]

The intent is explicit. “Give me combinations”. “Give me permutations”. The code is a statement, not a convoluted manual process.

The Revelation: Declare Your Intent, Not Your Algorithm

  • Guaranteed correctness: combinatorial logic is subtle and easy to get wrong. itertools gives you the correct implementation, every time.
  • Readability: combinations(items, 2) is infinitely clearer than two nested loops with index bookkeeping.
  • Performance: these functions are written in C and are incredibly fast. Anything you write in pure Python will be orders of magnitude slower.

Bend Reality: The Crucial Difference

Choose your weapon wisely.

  • combinations: use it when order doesn’t matter. A team of (Ana, Luis) is the same as (Luis, Ana). Ideal for forming groups, teams or fight pairings.
  • permutations: use it when order is crucial. A podium of (Gold: Ana, Silver: Luis) is different from (Gold: Luis, Silver: Ana). Ideal for rankings, line-ups or passwords.

Understanding this difference is the mark of someone who thinks about the problem before pounding the keyboard.

Trick #10: The for-else Loop, the Loop’s Final Cry

You search for an element in a list. If you find it, you do something and stop. If you walk the whole list and don’t find it, you do something else.

The Heresy: The Flag of Defeat

The novice’s method is a “flag” variable. A small, sad variable that keeps track of whether the search succeeded.

# A clumsy pattern that pollutes the code with needless state.
numeros = [1, 5, 9, 13, 17]
valor_a_buscar = 11
encontrado = False # The flag.

for n in numeros:
    if n == valor_a_buscar:
        print(f"¡Encontrado el {valor_a_buscar}!")
        encontrado = True
        break

if not encontrado:
    print(f"El {valor_a_buscar} no está en la lista.")

That encontrado flag is a nuisance. It is manual state you have to remember to initialise, flip and check. It pollutes the namespace and complicates simple logic.

The Master’s Path: The Natural Consequence

Python offers an elegant and often ignored syntax that removes the need for the flag entirely: the else block on a for loop.

numeros = [1, 5, 9, 13, 17]
valor_a_buscar = 11

for n in numeros:
    if n == valor_a_buscar:
        print(f"¡Encontrado el {valor_a_buscar}!")
        break
else:
    # This block only runs if the loop finishes naturally (no break).
    print(f"El {valor_a_buscar} no está en la lista.")

The else does not belong to the if. It belongs to the for. Its meaning is:

“If this loop completed every iteration and was never interrupted by a break, then run this code”.

The Revelation: for ... no-break

Think of for/else as for/then. It is a construct that ties an action to the full completion of an iteration.

  • It removes state: banish flag variables from your code. Less state to manage means simpler code and fewer bugs.
  • It expresses intent: the for/else structure says exactly what it means: “Search. If the search fails (the loop runs out), do this”. The intent is baked into the syntax of the language.

Bend Reality: The Prime Number Test

The canonical example of the beauty of for/else is the function that determines whether a number is prime.

def es_primo(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            # We found a divisor, so it is not prime.
            print(f"{n} no es primo, es divisible por {i}.")
            break
    else:
        # The loop ended without finding divisors. It is prime.
        print(f"{n} es un número primo.")

es_primo(13) # Output: 13 es un número primo.
es_primo(15) # Output: 15 no es primo, es divisible por 3.

Without the else clause you would need an es_primo_flag. With it, the logic is pure, clean and self-documenting.

Section 5: Secrets of Object-Oriented Programming, the Art of Giving Code Life

Plenty of people learn to type class and believe they understand Object-Oriented Programming (OOP).

That is a rookie mistake.

They see classes as simple folders for grouping variables and functions. A conceptual disaster.

A well-forged class is not a folder. It is a being. An entity with its own state, its own behaviour and its own rules.

The master does not write classes that are passive bags of data, he sculpts objects that are active, intelligent and self-sufficient agents.

In this section you will move past the syntax and learn the craft. You will discover how to control access to attributes without the verbosity of other languages, how to give your objects a voice so they can describe themselves, and how to optimise them so they stay light and efficient. Time to stop writing data containers and start creating life.

Trick #11: @property, the Attribute That Thinks

Attribute management is a battlefield. Either you leave them public and unprotected, or you follow the dogmas of other languages and build ugly cages of code.

The Heresy: Public Anarchy or the Getter/Setter Prison

The beginner commits one of two sins. The first is anarchy:

class Usuario:
    def __init__(self, edad):
        self.edad = edad

user = Usuario(25)
user.edad = -90 # Valid. Disastrous. Zero control.

The second sin, often imported from Java or C++, is building a prison of get_ and set_ methods.

# Verbose code, alien to Python.
class Usuario:
    def __init__(self, edad):
        self._edad = edad

    def get_edad(self):
        return self._edad

    def set_edad(self, valor):
        if valor < 0:
            raise ValueError("La edad no puede ser negativa")
        self._edad = valor

user = Usuario(25)
user.set_edad(30)
print(user.get_edad())

This code is an insult to Python.

It is clumsy, it forces the caller to use method calls for something that is conceptually a plain attribute, and it shouts “I don’t understand the philosophy of this language”.

The Master’s Path: Start Simple, Evolve With Power

The Pythonic path is to trust. You start with a simple public attribute. When the need for control shows up, you introduce it with magic, without changing the public interface.

class Usuario:
    def __init__(self, edad):
        self._edad = edad # "Private" attribute by convention

    @property
    def edad(self):
        """This is the 'getter'. Accessed as user.edad"""
        print("(Accediendo al valor...)")
        return self._edad

    @edad.setter
    def edad(self, valor):
        """This is the 'setter'. Used as user.edad = valor"""
        print(f"(Estableciendo valor a {valor}...)")
        if valor < 0:
            raise ValueError("La edad no puede ser negativa")
        self._edad = valor

user = Usuario(25)
user.edad = 30      # Calls the setter
print(user.edad)    # Calls the getter
# user.edad = -10     # Would raise ValueError

To the outside world, user.edad is still an attribute. Callers don’t know (and don’t care) that validation logic hides behind that simple assignment or read.

You evolved your class without breaking the contract with the people using it.

The Revelation: Protect the Interface, Not the Data

Python’s philosophy is not to build impassable walls (like private in other languages), it is to create clean, predictable interfaces. @property is the perfect tool for that.

  • Clean interface: you reach the data naturally (user.edad), not through verbose methods.
  • Safe refactoring: you can go from a public attribute to a property without breaking any code that uses your class. Evolution without revolution.
  • Real encapsulation: the validation logic lives inside the class, where it belongs.

Bend Reality: Read-Only and Computed Attributes

Need an attribute that can only be read? Simply don’t define a .setter.

class Circulo:
    def __init__(self, radio):
        self.radio = radio

    @property
    def area(self):
        """The area is computed, not stored. Read-only."""
        return 3.14159 * (self.radio ** 2)

c = Circulo(10)
print(c.area) # Looks like an attribute, but it is computed on the spot.
# c.area = 300 # AttributeError: can't set attribute

That is elegance. You expose a computed value as if it were a plain stored piece of data.

It is intuitive, clean and the mark of a true craftsman of classes.

Trick #12: __str__ vs __repr__, the Soul and the DNA of an Object

You create an object, put it in a list and print it. And Python hands you back useless garbage: [<__main__.MiClase object at 0x10a7f5b50>].

That is a sign of incompetence. Your object doesn’t know how to introduce itself.

The Heresy: The Mute Object

The most common mistake is implementing neither of these “magic” methods. The object is mute. It cannot describe itself.

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Punto(10, 20)
print(p) # Output: <__main__.Punto object at ...>  (Useless)

The object exists, but it has no voice. For a developer debugging code, it is a ghost.

The Master’s Path: Two Voices for Two Audiences

A masterful object has two ways of expressing itself. One for the programmer and one for the user.

  • __repr__(self): the unambiguous representation. Its goal is to be a complete, unambiguous description of the object, aimed at the developer. The golden rule: the result of __repr__ should ideally be valid Python code that could recreate the object.
  • __str__(self): the readable representation. Its goal is to be a friendly, clean description of the object, aimed at the end user via print() or str().
class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        # For the developer: precise and reproducible.
        return f"Punto(x={self.x}, y={self.y})"

    def __str__(self):
        # For the user: simple and readable.
        return f"({self.x}, {self.y})"

Now look at the behaviour:

p = Punto(10, 20)

print(p)           # Calls __str__. Output: (10, 20)
print(str(p))      # Calls __str__. Output: (10, 20)
print(repr(p))     # Calls __repr__. Output: Punto(x=10, y=20)

# The container (list) uses __repr__ for its elements. Crucial!
puntos = [Punto(1,2), Punto(3,4)]
print(puntos)
# Output: [Punto(x=1, y=2), Punto(x=3, y=4)]

The Revelation: Different Contexts, Different Answers

Understanding the difference is fundamental to writing usable, debuggable classes.

  • __repr__ is for developers: think of it as the object’s DNA. You use it in the debugger, in the logs, and it is what containers display. If you can only implement one, make it this one.
  • __str__ is for users: think of it as the object’s “name” or “face”. The pretty version. Python is polite: if you call print() and there is no __str__, it falls back to __repr__.

Bend Reality: The Rule of Thumb

  1. Always implement __repr__ in your classes. Make it informative and unambiguous. Your future self will thank you when you are debugging at 3 in the morning.
  2. Implement __str__ only when you need a “friendly” representation that differs from the developer one. If __repr__ is already readable enough, you may not need __str__ at all.

Section 6: Productivity Hacks and Modern Code, the Razor’s Edge

The programmer’s craft is not static. The language evolves.

New tools get forged, sharper and more precise. Clinging to the old ways is not a sign of loyalty, it is a symptom of intellectual laziness.

A true master does not fear the new. He examines it, puts it on trial and, if it proves superior, adopts it without hesitation.

This last section is about that edge.

These are the techniques and syntax that appeared in recent Python versions. Ignoring them means choosing to fight with a bronze sword in the age of steel. Mastering them gives you a decisive advantage in speed, clarity and robustness.

Trick #13: Advanced F-strings, the Art of Interpolation

For years, formatting strings in Python was a clumsy ritual. A battlefield between competing styles, each with its own warts.

The Heresy: The Formatting Fossils

The novice, or the programmer stuck in the past, still reaches for archaic methods. Concatenation with +, prone to type errors. The % operator, a relic inherited from C, functional but awkward. Or the .format() method, verbose and visually disconnected from the string it modifies.

# A museum of formatting horrors.
nombre = "Arturo"
edad = 42

# Concatenation (slow and fragile)
print("El usuario " + nombre + " tiene " + str(edad) + " años.")

# % operator (archaic)
print("El usuario %s tiene %d años." % (nombre, edad))

# .format() method (verbose)
print("El usuario {} tiene {} años.".format(nombre, edad))

Any of these methods shouts:

“Either I am new here, or I haven’t bothered to learn anything in the last five years”.

The Master’s Path: The Direct Expression

Since Python 3.6 there is only one correct way to format strings for 99% of cases: f-strings (formatted string literals).

nombre = "Arturo"
edad = 42

print(f"El usuario {nombre} tiene {edad} años.")

Look at it. The variable lives inside the string. The leading f switches the mechanism on.

It is concise, readable and, in most cases, the fastest method available. There is no excuse for skipping it.

The Revelation: Context Is King

The superiority of f-strings comes from the expression living exactly where it is used. You don’t have to jump with your eyes to the end of the line to find a .format(var1, var2) or a % (var1, var2).

The expression f"Usuario: {nombre}" reads naturally. It lowers cognitive load and makes the code easier to scan. End of discussion.

Bend Reality: The Hidden Debugger and Precision Formatting

F-strings are not only for displaying variables. You can drop complex expressions inside them and control the format with surgical precision.

import datetime

# Complex expressions
print(f"El doble de la edad es {edad * 2}.")

# Format control (float with 2 decimals)
precio = 19.99
print(f"El precio es {precio:.2f} €.")

# Zero padding
numero_factura = 8
print(f"Factura Nº {numero_factura:04d}.") # Output: Factura Nº 0008.

# Date formatting
hoy = datetime.date.today()
print(f"Hoy es {hoy:%d-%m-%Y}.")

# The ultimate debugging trick (Python 3.8+)
# The '=' sign adds the variable name along with its value.
print(f"Depurando: {nombre=} {edad=}")
# Output: Depurando: nombre='Arturo' edad=42

Master these micro-syntaxes. They are the difference between using a tool and squeezing everything out of it.

Trick #14: The Walrus Operator :=, Assignment and Condition in One Strike

There is a pattern that repeats over and over: get a value, check whether that value is valid and, if it is, use it.

The Heresy: The Double Step

The traditional approach needs two steps. First you assign. Then you check.

# A common pattern when reading a file line by line.
linea = f.readline()
while linea:
    procesar(linea)
    linea = f.readline()

# Another example: a function that may return None.
resultado = funcion_costosa()
if resultado:
    usar(resultado)

This code is redundant. The call to f.readline() or funcion_costosa() shows up twice, or on a separate line that breaks the logical flow of the if or the while.

The Master’s Path: The Combined Strike

Introduced in Python 3.8, the assignment expression operator, nicknamed “walrus” for its := looks, lets you do both things at once.

# Assigns 'linea' AND checks whether it is truthy in the same expression.
while linea := f.readline():
    procesar(linea)

# Assigns 'resultado' AND checks that it is not None.
if resultado := funcion_costosa():
    usar(resultado)

The value is assigned to the variable and the whole expression evaluates to that same value.

The Revelation: Group the Relevant Logic

The walrus is not about saving lines. It is about improving the cognitive flow. It lets the retrieval of a value and the decision about that value happen in the same place.

In while loops that read from a stream (files, sockets), or in complex list comprehensions, this operator can make the code considerably clearer by reducing the need to declare variables outside the context where they are used.

Bend Reality: The Master’s Discipline

The power of the walrus is subtle and invites abuse. A master uses it sparingly.

An amateur, drunk on novelty, will jam it everywhere and produce unreadable lines of code.

The rule is simple:

use it when it simplifies a repetitive structure (like the while above) or when it avoids a duplicated function call. If it makes a line harder to read, it is the wrong tool. Clarity always beats cryptic brevity. Don’t be that programmer.

Trick #15: pathlib, the Path of the Object

Handling file paths with text strings is an endless source of bugs and ugly code.

The Heresy: String Soup and os.path

The archaic method treats paths as plain strings, using os.path.join and other functions from the os module to manipulate them.

import os

# Clumsy, error-prone code.
dir_actual = os.path.dirname(__file__)
nombre_fichero = "datos.csv"
ruta_completa = os.path.join(dir_actual, "recursos", nombre_fichero)

if os.path.exists(ruta_completa):
    with open(ruta_completa, 'r') as f:
        # ...

This approach is procedural, not object-oriented. It keeps the data (the path as a string) and the operations (the os.path functions) in separate places.

The Master’s Path: Treat Paths as What They Are

The modern, correct way to handle paths is the pathlib module.

from pathlib import Path

# Clean, intuitive, object-oriented code.
dir_actual = Path(__file__).parent
ruta_completa = dir_actual / "recursos" / "datos.csv"

if ruta_completa.exists():
    with ruta_completa.open('r') as f:
        # ...

Look at the beauty of it.

The slash / is overloaded to join paths intelligently and portably across operating systems. The path is no longer a string, it is a Path object with its own methods.

The Revelation: Objects Over Strings

pathlib is a philosophy. It is the embodiment of the principle that data and the operations acting on it should live together.

  • Intuitive: joining paths with / feels natural. Methods like ruta.parent, ruta.name, ruta.suffix explain themselves.
  • Powerful: Path objects have methods for everything: ruta.is_dir(), ruta.read_text(), ruta.write_text(), ruta.glob('*.txt').
  • Robust: the module handles operating system differences (like \ vs /) transparently.

Bend Reality: File Handling in One Line

With pathlib, many common file operations collapse into a single line, removing the need for an explicit with open(...) for simple tasks.

from pathlib import Path

ruta = Path("mi_fichero.txt")

# Write to a file
ruta.write_text("Hola, mundo.")

# Read from a file
contenido = ruta.read_text()

# Change a file extension
nueva_ruta = ruta.with_suffix(".md")
print(nueva_ruta) # Output: mi_fichero.md

Stop manipulating strings. Start handling Path objects. It is the mark of a professional who picks the right, modern tool for the job.

The End of the Learning

You have reached the last page.

The lessons are over. If you came here looking for a diploma, you picked the wrong book.

The truth is that these 15 tricks have not made you a master. They have only taken away your excuses for staying an amateur. A pile of bricks is not a house, and a collection of techniques does not make you a programmer. It is the judgement to know when and how to lay them that defines the builder.

From now on, your job is not to remember. It is to understand.

Real learning starts when you close this book and open your own old code. Look at it. If you don’t feel a stab of shame, you learned nothing. Analyse it without mercy, like an engineer inspecting a cracked structure. Find the weak points, the clumsy logic, the code written out of laziness. And fix it. Not because you have to, but because it is your craft.

Stop looking for shortcuts.

Elegance is not born of inspiration, it is born of discipline.

It is born of deleting a solution that works but confuses, in order to rewrite it in a way that holds.

It is born of resisting the temptation to stack patches and reinforcing the foundations instead.

The world is already drowning in code that works by accident, kept alive with faith and patches. It is fragile, it is expensive and it is a burden on whoever comes next. Your duty, now that you know a better way, is to stop adding more junk to that pile.

The work is waiting.

How Did You Get Here?

This content was sent to the newsletter.

You can join the email list here: vamosallio.com