Xray AI Lab
Python 33 min readBy Nbark Kazdabi | Xray 0xFF

Python Programming: A Complete Guide to Concepts, Examples, and Exercises

Learn Python with worked examples: functions, lambdas, recursion, classes, number problems, series, and patterns, plus 28 graded exercises with solutions.

Artical cover picture python for data science beginners

Python is the language most people reach for when they start in programming, data science, or machine learning. Its syntax is short, its standard library is large, and the same handful of ideas (functions, objects, loops, and clear problem decomposition) carry from a first script all the way to production data pipelines.

This guide builds those ideas through the kind of problems that appear in university labs and technical interviews: functions with flexible parameters, lambdas, recursion, classes and operator overloading, classic number problems, mathematical series, and pattern printing. Every concept follows the same rhythm: what it is, its syntax, a small example, the output, common mistakes, and a harder example. A graded practice section at the end collects exercises with hints and full solutions.

What you will learn

  • How to read input, print precisely, branch, and loop.
  • How default, variable-length, and keyword arguments really work.
  • When lambdas help and when they hurt, and how recursion maps to a problem.
  • How instance variables, multiple inheritance, and operator overloading behave.
  • How to turn a number problem or a series into a clean loop (or a formula).
  • How to design any text pattern from row and column indexes.

Table of Contents

How to Use This Guide

All examples are plain Python 3 and need nothing beyond the standard library. Save a snippet as example.py and run python example.py, or paste it into a Jupyter notebook cell. For examples that call input(), the output shows the typed answer right after the prompt, as it would look in a terminal.

Two habits will speed up your learning more than anything else:

  1. Predict the output before you run the code. Wrong predictions are where learning happens.
  2. Change one thing and rerun. Edit a number, remove a line, and watch what breaks.

For the official reference, keep the Python tutorial and the standard library reference open in another tab.

Python Basics You Need First

The later sections assume you are comfortable with five small building blocks. If they are familiar, skim and move on.

Reading Input and Converting Types

input() always returns a string, even when the user types digits. To calculate with the value you must convert it with int() or float().

num1 = int(input("Enter 1st number: "))
num2 = int(input("Enter 2nd number: "))
print("num1 + num2 =", num1 + num2)

Output:

Enter 1st number: 3333
Enter 2nd number: 666
num1 + num2 = 3999
  • int(input(...)) first reads text, then converts it to an integer.
  • print accepts several values and separates them with a space.

Common mistake: forgetting the conversion. Strings are concatenated, not added.

a = input("a: ")
b = input("b: ")
print(a + b)       # "3" + "4" is the string "34"
print(int(a) + int(b))

Output:

a: 3
b: 4
34
7

If the user types something that is not a number, int() raises ValueError. Production code wraps the call in try/except; you will see that in exercise B1.

Printing with end and f-strings

By default print adds a newline. The end parameter changes that, which is the key to printing rows of a pattern one item at a time.

for j in range(1, 6):
    print(j, end=" ")
print()   # finish the line

Output:

1 2 3 4 5 

An f-string embeds expressions directly in text, and a format spec such as .2f controls decimals.

name, score = "Aya", 92.456
print(f"{name} scored {score:.2f}")
print(f"{7} x {8} = {7 * 8}")

Output:

Aya scored 92.46
7 x 8 = 56

Conditions with if, elif, and else

A condition runs a block only when its test is true. Python checks branches from top to bottom and runs the first one that matches.

def describe(n):
    if n < 0:
        return "negative"
    elif n == 0:
        return "zero"
    else:
        return "positive"

for value in (-5, 0, 7):
    print(value, "is", describe(value))

Output:

-5 is negative
0 is zero
7 is positive

Common mistakes: writing = (assignment) instead of == (comparison), and forgetting that indentation defines the block.

Loops with for, while, and range

for repeats over a sequence; range(start, stop) produces numbers from start up to, but not including, stop. while repeats as long as a condition holds, which suits problems where you do not know the iteration count in advance, such as extracting digits.

n = 145
digits = []
while n != 0:
    digits.append(n % 10)   # last digit
    n //= 10                # drop the last digit
print(digits)

Output:

[5, 4, 1]

The pair % 10 and // 10 is the standard way to peel digits off an integer. Remember it: it returns in the number problems below.

Lists and Type Checks

A list can hold values of different types. Sometimes you need to keep only one type, such as the integers in a mixed list.

items = ["abc", 34.56, 32, 3 + 4j, "b", 55, 65.7, "90", 180]
ints = []
for element in items:
    if type(element) == int:
        ints.append(element)
print(ints)

Output:

[32, 55, 180]

Note that "90" is a string and 34.56 is a float, so neither is kept. There is a subtle trap: bool is a subclass of int, so isinstance(True, int) is True.

mixed = [1, True, 2.0, 3, False]
print([x for x in mixed if type(x) is int])      # exact type
print([x for x in mixed if isinstance(x, int)])  # includes bool

Output:

[1, 3]
[1, True, 3, False]

Use type(x) is int when you want exactly integers, and isinstance when subclasses are welcome. The bracketed one-line form is a list comprehension, a compact replacement for a loop that builds a list.

Functions

A function packages reusable logic behind a name. It receives parameters, may return a value, and keeps its own local variables. See the official section on defining functions.

Defining Functions and Returning Values

def rectangle_area(width, height):
    return width * height

print(rectangle_area(4, 5))
print(rectangle_area(height=2, width=10))   # keyword arguments, any order

Output:

20
20

A function without return gives back None. Using return ends the function immediately.

Default Arguments

A parameter with a value in the definition is optional. If the caller omits it, the default is used.

def add(a, b, c=5):      # c is a default argument
    return a + b + c

print("The addition is:", add(2, 3))
print("The addition is:", add(2, 3, 10))

Output:

The addition is: 10
The addition is: 15

Parameters without defaults must come before parameters with defaults; the reverse is a syntax error.

def broken(a=1, b):      # SyntaxError: parameter without a default follows parameter with a default
    return a + b

A famous trap: mutable defaults. The default value is created once, when the function is defined, not on every call.

def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

print(add_item("a"))
print(add_item("b"))     # the same list is reused!

def add_item_safe(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(add_item_safe("a"))
print(add_item_safe("b"))

Output:

['a']
['a', 'b']
['a']
['b']

Use None as the default and create the mutable object inside the function.

Variable Positional Arguments

Sometimes you do not know how many values a caller will pass. Prefix a parameter with * and Python packs the extra positional values into a tuple.

def total(*numbers):
    return sum(numbers)

print(total(4, 8, 15))
print(total())            # an empty tuple sums to 0
values = [1, 2, 3]
print(total(*values))     # * in a call unpacks a list into separate arguments

Output:

27
0
6

By convention the parameter is named args, but any name works.

Variable Keyword Arguments

Prefix a parameter with ** and Python packs the extra named arguments into a dictionary. This is handy when the names themselves carry meaning, such as subject names and grades.

def average_of(**scores):
    values = list(scores.values())
    if not values:
        return 0.0
    return sum(values) / len(values)

print(f"Average is: {average_of(math=14, physics=11, informatics=17):.2f}")
print(f"Average is: {average_of(q1=5, q2=10, q3=15, q4=20):.2f}")
print(f"Average is: {average_of():.2f}")

Output:

Average is: 14.00
Average is: 12.50
Average is: 0.00

The guard for an empty dictionary prevents a ZeroDivisionError, a typical edge case worth handling.

Combining Every Parameter Kind

Python fixes the order: regular parameters, then defaults, then *args, then **kwargs. The diagram shows how one call is distributed.

Diagram showing how the call show(1, 2, 3, 5, 6, 7, color="red") is split into a, b, c, the extras tuple, and the options dictionary

Figure 1: How Python binds positional and keyword arguments to parameters.

def show(a, b, c=3, *extras, **options):
    print("a, b, c =", a, b, c)
    print("extras  =", extras)
    print("options =", options)

show(1, 2, 3, 5, 6, 7, color="red", size=2)
show(1, 2)
show(1, 2, 3, "tag", unit="cm")

Output:

a, b, c = 1 2 3
extras  = (5, 6, 7)
options = {'color': 'red', 'size': 2}
a, b, c = 1 2 3
extras  = ()
options = {}
a, b, c = 1 2 3
extras  = ('tag',)
options = {'unit': 'cm'}

Notice that in the second call extras is an empty tuple () and options is an empty dictionary {}: they always exist, they are just empty.

Lambda Functions

A lambda is a small anonymous function limited to a single expression, whose value is returned automatically.

add = lambda a, b: a + b
print(add(3, 7))
print((lambda a, b: a + b)(3, 7))   # defined and called in one expression

Output:

10
10

Lambdas shine as short throwaway arguments, for example a sort key:

students = [("Aya", 17), ("Omar", 14), ("Lina", 15)]
print(sorted(students, key=lambda s: s[1]))

Output:

[('Omar', 14), ('Lina', 15), ('Aya', 17)]

A lambda cannot contain statements such as loops or return, so a factorial needs a helper such as functools.reduce, which folds a sequence into one value:

from functools import reduce

factorial = lambda n: reduce(lambda acc, k: acc * k, range(1, n + 1), 1)
print(factorial(5), factorial(0))

Output:

120 1

Style note: assigning a lambda to a name (as above) is fine for learning, but PEP 8 recommends a normal def when a function deserves a name, because it produces better error messages.

Recursion and the GCD

A function is recursive when it calls itself on a smaller version of the problem. Every recursive function needs a base case that stops the calls.

The greatest common divisor (GCD) has a neat property: if a > b, then gcd(a, b) == gcd(a - b, b), and symmetrically for b > a. Repeating this shrinks the numbers until they are equal, and that value is the GCD.

Call trace of the subtraction-based GCD for 235 and 875, ending at 5

Figure 2: Each call replaces the larger number with the difference until both are equal.

def gcd_subtract(a, b):
    if a == 0 or a == b:    # base cases
        return b
    if b == 0:
        return a
    if a > b:
        return gcd_subtract(a - b, b)
    return gcd_subtract(a, b - a)

print(gcd_subtract(235, 875))

Output:

5

This works but it is slow when one number is much larger than the other: gcd_subtract(1, 1_000_000) needs a million calls and exceeds Python’s recursion limit. Replacing repeated subtraction by the remainder operator gives Euclid’s algorithm, which needs only about log(min(a, b)) steps.

def gcd_subtract(a, b):
    if a == 0 or a == b:
        return b
    if b == 0:
        return a
    if a > b:
        return gcd_subtract(a - b, b)
    return gcd_subtract(a, b - a)

def gcd_euclid(a, b):
    while b:
        a, b = b, a % b
    return a

try:
    gcd_subtract(1, 1_000_000)
except RecursionError:
    print("subtraction version: RecursionError")
print("Euclid version:", gcd_euclid(1, 1_000_000))
print("235 and 875:", gcd_euclid(235, 875))

Output:

subtraction version: RecursionError
Euclid version: 1
235 and 875: 5

Edge cases to test: gcd(0, n) (answer n), equal numbers, and one number dividing the other.

Classes and Objects

A class is a blueprint; an object (instance) is something built from it. Data stored on an object is called an instance variable. Read the official tutorial on classes for the full picture.

Constructors and Instance Variables

__init__ is the constructor: Python calls it automatically when you create an object. self is the object being built.

class Sensor:
    def __init__(self, name, unit="C"):
        self.name = name
        self.unit = unit

    def describe(self):
        return f"{self.name} measures in {self.unit}"

temp = Sensor("thermo-1")
hum = Sensor("hygro-1", unit="%")
print(temp.describe())
print(hum.describe())

Output:

thermo-1 measures in C
hygro-1 measures in %

You can also write __init__(self) with no extra parameters and give every object the same starting values; the caller then writes Sensor() with empty parentheses.

Inspecting an Object with dict

Instance variables can be created in three places: inside __init__, inside any other method (only when that method runs), or from outside the class. The __dict__ attribute shows what an object currently holds.

Two objects created from one class; the first has attributes a, b, c and the second only a

Figure 3: Instance variables belong to each object, not to the class.

class Demo:
    def __init__(self):
        self.a = 5          # created with the object

    def add_b(self):
        self.b = 10         # created only when add_b() runs

first = Demo()
second = Demo()
first.add_b()
first.c = 15                # created from outside the class
print(first.__dict__)
print(second.__dict__)

Output:

{'a': 5, 'b': 10, 'c': 15}
{'a': 5}

Only first received b and c. Common mistake: reading an attribute that was never created.

class Box:
    def __init__(self):
        self.a = 1

    def set_b(self):
        self.b = 2

box = Box()
try:
    print(box.b)
except AttributeError as error:
    print("AttributeError:", error)

Output:

AttributeError: 'Box' object has no attribute 'b'

Good practice: create every instance variable in __init__ so each object always has the same shape.

Multiple Inheritance

When a class inherits from more than one parent, it is using multiple inheritance, and Python supports it. List the parents in parentheses.

class Sensor:
    def read(self):
        return "reading data"

class Logger:
    def log(self, message):
        return f"LOG: {message}"

class SmartSensor(Sensor, Logger):
    pass

device = SmartSensor()
print(device.read())
print(device.log("started"))

Output:

reading data
LOG: started

When two parents define the same method name, Python needs a rule to decide which wins. That rule is the method resolution order (MRO): the order in which classes are searched, left to right with parents after children. You can print it, and the MRO how-to explains the algorithm.

A diamond-shaped inheritance diagram with Base at the top, A and B below it, and C at the bottom, and the resulting MRO C, A, B, Base, object

Figure 4: The diamond problem and the order Python searches for a method.

class Base:
    def hello(self):
        return "Base"

class A(Base):
    def hello(self):
        return "A -> " + super().hello()

class B(Base):
    def hello(self):
        return "B -> " + super().hello()

class C(A, B):
    pass

print(C().hello())
print([cls.__name__ for cls in C.__mro__])

Output:

A -> B -> Base
['C', 'A', 'B', 'Base', 'object']

super() does not mean “the parent”: it means “the next class in the MRO”. That is why A calls B, even though B is not a parent of A.

Operator Overloading

Special “dunder” methods let your objects work with operators. __add__ powers +, __mul__ powers *, __rmul__ handles 3 * obj when the left operand does not know your class, and __repr__ controls how the object prints. See the data model reference.

Flow diagram mapping v1 + v2 to add, v1 * 3 to mul, and 3 * v1 to rmul

Figure 5: Which special method Python calls for each expression.

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

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, other):
        if isinstance(other, Vector):          # element-wise product
            return Vector(self.x * other.x, self.y * other.y)
        return Vector(self.x * other, self.y * other)   # scalar product

    def __rmul__(self, other):                 # handles 3 * v
        return self * other

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(3, 4)
print(v1 + v2)
print(v1 * v2)
print(v1 * 3)
print(3 * v1)

Output:

Vector(5, 7)
Vector(6, 12)
Vector(6, 9)
Vector(6, 9)

Common mistakes: forgetting __rmul__ (then 3 * v1 raises TypeError), and assuming v1 * v2 is a dot product. Here it multiplies component by component; a dot product would be a different, explicitly named method.

Number Problems

Many classic exercises ask you to test a property of an integer. The method is always the same: translate the definition into a loop or a formula, then test edge cases (0, 1, negatives, and large inputs).

Finding Factors

A factor of n is a number that divides it with no remainder. Test every candidate from 1 to n with the remainder operator.

n = int(input("Enter a number: "))
factors = [i for i in range(1, n + 1) if n % i == 0]
print(factors)

Output:

Enter a number: 45
[1, 3, 5, 9, 15, 45]

This takes n steps. The optimized version appears in exercise B7.

Prime Numbers

A prime is an integer greater than 1 whose only factors are 1 and itself. If a number has a factor larger than its square root, it must also have a smaller partner factor, so testing up to math.isqrt(n) is enough: roughly √n steps instead of n.

import math

def is_prime(n):
    if n < 2:
        return False
    for divisor in range(2, math.isqrt(n) + 1):
        if n % divisor == 0:
            return False
    return True

print([n for n in range(1, 31) if is_prime(n)])
print(is_prime(0), is_prime(1), is_prime(2), is_prime(97))

Output:

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
False False True True

The tests for 0, 1, and 2 matter: 0 and 1 are not prime, and 2 is the only even prime.

More number problems (perfect and abundant numbers, pronic numbers, strong numbers, automorphic numbers) are in the practice section.

Arithmetic and Geometric Series

A series problem asks for the sum of the first N terms. The reliable method is to find the formula for term i, then add it in a loop.

  • Arithmetic series: each term adds a fixed step. Term i is first + (i - 1) * step.
  • Geometric series: each term multiplies by a fixed ratio. Term i is first * ratio ** (i - 1).
def arithmetic_sum(first, step, terms):
    total = 0
    for i in range(1, terms + 1):
        total += first + (i - 1) * step
    return total

print(arithmetic_sum(1, 1, 20))     # 1 + 2 + ... + 20
print(arithmetic_sum(9, 4, 20))     # 9 + 13 + 17 + ...
print(arithmetic_sum(2, 2, 20))     # 2 + 4 + 6 + ... (20 terms)
print(arithmetic_sum(10, -1, 10))   # 10 + 9 + 8 + ... + 1

Output:

210
940
420
55

The closed form terms / 2 * (2 * first + (terms - 1) * step) gives the same answer without a loop, which is a good cross-check:

def arithmetic_sum_formula(first, step, terms):
    return terms * (2 * first + (terms - 1) * step) // 2

print(arithmetic_sum_formula(9, 4, 20))

Output:

940

Geometric series work the same way. Keep the term and the running total separate:

def geometric_sum(first, ratio, terms):
    total = 0
    term = first
    for _ in range(terms):
        total += term
        term *= ratio
    return total

print(geometric_sum(2, 2, 20))      # 2 + 4 + 8 + ...
print(geometric_sum(2, 3, 20))      # 2 + 6 + 18 + ...
print(geometric_sum(10, 3, 20))     # 10 + 30 + 90 + ...
print(2 * (2 ** 20 - 1))            # formula: first * (ratio**n - 1) / (ratio - 1)

Output:

2097150
3486784400
17433922000
2097150

Fractional and alternating series use the same pattern with a more complex term. Examples are in exercises I6, I7, and A8.

Pattern Printing

Pattern problems look like puzzles but follow one method: a pattern is a table of rows and columns, so use an outer loop for rows (i) and an inner loop for columns (j). Decide what each cell prints from i and j.

Rows, Columns, and the Index Map

A 5 by 5 grid with row index i down the side and column index j across the top, highlighting the main diagonal where i equals j

Figure 6: Useful conditions for an n by n grid, with n = 5.

PositionCondition
First rowi == 0
Last rowi == n - 1
First columnj == 0
Last columnj == n - 1
Middle row or columni == n // 2 or j == n // 2
Main diagonali == j
Anti-diagonali + j == n - 1

Start with the simplest grid: a square.

n = 5
for i in range(n):
    for j in range(n):
        print("*", end=" ")
    print()      # new line after each row

Output:

* * * * * 
* * * * * 
* * * * * 
* * * * * 
* * * * * 

The print() at the end of the outer loop is easy to forget; without it all rows join into one line.

Using the conditions in the table, an if inside the inner loop turns the square into a hollow square or a diagonal:

n = 5
for i in range(n):
    for j in range(n):
        if i == 0 or i == n - 1 or j == 0 or j == n - 1:
            print("*", end=" ")
        else:
            print(" ", end=" ")
    print()

Output:

* * * * * 
*       * 
*       * 
*       * 
* * * * * 

Triangles of Stars and Numbers

For a right triangle, row i simply prints i + 1 cells, so the inner loop’s range depends on the outer variable.

n = 5
for i in range(1, n + 1):
    for j in range(i):
        print("*", end=" ")
    print()

Output:

* 
* * 
* * * 
* * * * 
* * * * * 

Printing the column number instead of a star gives a number triangle:

n = 5
for i in range(1, n + 1):
    for j in range(1, i + 1):
        print(j, end=" ")
    print()

Output:

1 
1 2 
1 2 3 
1 2 3 4 
1 2 3 4 5 

Floyd’s triangle keeps counting across rows, so it needs a counter that is not reset inside the outer loop:

n = 5
counter = 1
for i in range(1, n + 1):
    for _ in range(i):
        print(counter, end=" ")
        counter += 1
    print()

Output:

1 
2 3 
4 5 6 
7 8 9 10 
11 12 13 14 15 

A common mistake is writing i += 1 at the end of a for loop body. It has no effect, because for reassigns i from the range on the next turn.

Letter Patterns with chr and ord

Characters have numeric codes: ord("A") is 65 and chr(65) is "A". Letters are consecutive, so chr(65 + k) is the k-th letter of the alphabet, which lets you reuse every number pattern for letters.

n = 5
for i in range(n):
    for j in range(i + 1):
        print(chr(65 + i), end=" ")     # row i repeats letter i
    print()

print()

code = 0
for i in range(1, n + 1):               # Floyd's triangle with letters
    for _ in range(i):
        print(chr(65 + code % 26), end=" ")
        code += 1
    print()

Output:

A 
B B 
C C C 
D D D D 
E E E E E 

A 
B C 
D E F 
G H I J 
K L M N O 

The % 26 makes the sequence wrap back to A after Z; without it, patterns with many rows would print symbols instead of letters. See chr() in the docs.

Centered Pyramids

A centered pyramid is a triangle with leading spaces. Each row has two jobs: print the spaces, then print the content. The diagram shows the anatomy of one row.

Anatomy of a pyramid row: leading spaces, then ascending letters, using the row ABCDE as an example

Figure 7: A pyramid row is leading spaces plus content.

Because each printed cell occupies two characters (a letter and a space), each missing cell is two spaces. For the letter pyramid A, A B C, A B C D E, … row i has 2 * i + 1 letters.

n = 5
for i in range(n):
    indent = " " * (2 * (n - 1 - i))
    letters = [chr(65 + k) for k in range(2 * i + 1)]
    print(indent + " ".join(letters))

Output:

        A
      A B C
    A B C D E
  A B C D E F G
A B C D E F G H I

" ".join(list) glues items with a space between them and no trailing space, which is cleaner than print(x, end=" ").

Alternating Characters

The row number can choose what to print. In this inverted triangle, even rows print z and odd rows print o, and each row is shorter than the last:

n = 6
for i in range(n):
    ch = "z" if i % 2 == 0 else "o"
    count = 2 * (n - i) - 1
    print(" " * (2 * i) + " ".join([ch] * count))

Output:

z z z z z z z z z z z
  o o o o o o o o o
    z z z z z z z
      o o o o o
        z z z
          o

[ch] * count builds a list with count copies of ch. More pattern exercises (right-aligned triangles, diamonds, palindromic pyramids, and patterns built from a word) are in the practice section.

Strings and ASCII Codes

Strings can be indexed (s[0]), sliced (s[1:4]), and reversed with the slice s[::-1]. Combined with chr and int, this solves a classic decoding puzzle.

Problem: a message was encoded by converting each character to its ASCII code, concatenating the digits, and reversing the whole string. Decode it. Only letters (A-Z = 65-90, a-z = 97-122) and spaces (32) occur.

Pipeline: reverse the digit string, split into ASCII codes, convert each code with chr, join the characters

Figure 8: Decoding a reversed string of ASCII codes.

Logic. After reversing, read codes from left to right. A code has two or three digits. Codes 32, 65-90, and 97-99 have two digits; codes 100-122 have three. The three-digit codes begin with 10, 11, or 12, which are never valid two-digit codes, so one simple rule is unambiguous: if the next two digits form a valid two-digit code, take two digits; otherwise take three.

def encode(text):
    return "".join(str(ord(ch)) for ch in text)[::-1]

def decode(encoded):
    digits = encoded[::-1]
    result = []
    i = 0
    while i < len(digits):
        two = int(digits[i:i + 2])
        if two == 32 or 65 <= two <= 90 or 97 <= two <= 99:
            result.append(chr(two))
            i += 2
        else:
            three = int(digits[i:i + 3])
            if not 100 <= three <= 122:
                raise ValueError(f"invalid code near position {i}")
            result.append(chr(three))
            i += 3
    return "".join(result)

secret = encode("PrepInsta")
print(secret)
print(decode(secret))
print(decode(encode("Hello World")))

Output:

796115110113721110141108
PrepInsta
Hello World

The loop reads each digit once, so the time is O(n). Edge cases: an empty string (returns "") and malformed input (raises ValueError instead of printing garbage).

Common Mistakes Worth Learning From

Short solutions that pass one test can still be wrong. These bugs appear again and again in interview solutions; each is shown with its fix.

1. Multiplying inside the sum (off by one term). To sum 2 + 4 + 8 + ..., this code adds a * 2 instead of a, so the series silently starts at 4:

total, a = 0, 2
for _ in range(20):
    total += a * 2      # wrong: adds the NEXT term
    a *= 2
print("buggy:", total)

total, a = 0, 2
for _ in range(20):
    total += a          # correct: add the current term, then advance
    a *= 2
print("fixed:", total)

Output:

buggy: 4194300
fixed: 2097150

2. Wrong ratio for the series. For 5 + 25 + 125 + ... the ratio is 5. Using a = a * 3 (copied from another series) runs without any error but computes 5 + 15 + 45 + .... Always check the first three printed terms against the series before trusting the sum.

3. Typos in variable names. Python only reports a misspelled name when the line runs:

result2 = 27.5
try:
    print(resutl2)
except NameError as error:
    print("NameError:", error)

Output:

NameError: name 'resutl2' is not defined

4. Reusing the loop variable of an outer loop. Writing for i in ... inside another for i in ... often works by accident, but it hides intent and breaks as soon as you use i after the inner loop. Use i, j, k for nested loops.

5. Stale outputs. In a notebook, a cell’s output may come from an earlier version of the code. Restart the kernel and run all cells before you trust a result.

Practice Exercises

Try each exercise on your own before opening the hint, and open the solution last. Exercises are grouped by difficulty.

Beginner Exercises

Exercise B1: Safe Sum of Two Numbers

Problem: Read two numbers from the user and print their sum. If either value is not a whole number, print Please enter whole numbers.

Example: input 12 and 30 gives Sum: 42.

Hint

int() raises ValueError for text such as "abc". Catch it with try/except.

Solution
def read_int(prompt):
    try:
        return int(input(prompt))
    except ValueError:
        return None

a = read_int("First number: ")
b = read_int("Second number: ")

if a is None or b is None:
    print("Please enter whole numbers.")
else:
    print("Sum:", a + b)

Output:

First number: 12
Second number: 30
Sum: 42

read_int returns None when conversion fails, so the main code can check both values at once. Typing abc for either prompt triggers the message.

Exercise B2: Greeting with Default Arguments

Problem: Write greet(name, greeting="Hello", punctuation="!") that returns a greeting string.

Example: greet("Aya") returns Hello, Aya!; greet("Omar", "Welcome", ".") returns Welcome, Omar.

Hint

Use an f-string and keep defaulted parameters after name.

Solution
def greet(name, greeting="Hello", punctuation="!"):
    return f"{greeting}, {name}{punctuation}"

print(greet("Aya"))
print(greet("Omar", "Welcome", "."))
print(greet("Lina", punctuation="?"))

Output:

Hello, Aya!
Welcome, Omar.
Hello, Lina?

Keyword arguments let the caller skip greeting and set only punctuation.

Exercise B3: Keep Only the Strings

Problem: From a mixed list, return a new list containing only the strings.

Example: ["abc", 34.56, 32, "b", 55] gives ['abc', 'b'].

Hint

isinstance(x, str) is true for strings. A list comprehension keeps the code short.

Solution
def only_strings(items):
    return [x for x in items if isinstance(x, str)]

print(only_strings(["abc", 34.56, 32, "b", 55, "90"]))
print(only_strings([]))

Output:

['abc', 'b', '90']
[]

Unlike the integer case, there is no subclass trap here. The empty-list test confirms an empty input produces an empty result.

Exercise B4: Average of Any Number of Values

Problem: Write average(*numbers). Raise ValueError if no numbers are given.

Example: average(4, 8, 15) gives 9.0.

Hint

numbers is a tuple. Check len(numbers) before dividing.

Solution
def average(*numbers):
    if not numbers:
        raise ValueError("average() needs at least one number")
    return sum(numbers) / len(numbers)

print(average(4, 8, 15))
try:
    average()
except ValueError as error:
    print("ValueError:", error)

Output:

9.0
ValueError: average() needs at least one number

Raising an error is better than returning 0, because an average of nothing is undefined, not zero.

Exercise B5: Right Triangle of Stars

Problem: Write star_triangle(n) that prints n rows, where row i has i stars separated by spaces.

Example (n = 4):

*
* *
* * *
* * * *
Hint

"*" repeated i times, joined by spaces: " ".join("*" * i).

Solution
def star_triangle(n):
    for i in range(1, n + 1):
        print(" ".join("*" * i))

star_triangle(4)

Output:

*
* *
* * *
* * * *

The row number equals the star count, so no inner loop is needed. For n = 0 nothing prints.

Exercise B6: Number Triangle

Problem: For n = 5, print rows 1, 1 2, 1 2 3, 1 2 3 4, 1 2 3 4 5.

Hint

The inner loop runs j from 1 to i. Or unpack a range with print(*range(1, i + 1)).

Solution
n = 5
for i in range(1, n + 1):
    print(*range(1, i + 1))

Output:

1
1 2
1 2 3
1 2 3 4
1 2 3 4 5

*range(...) unpacks the numbers as separate arguments, and print places a space between them by default.

Exercise B7: Factors in Square-Root Time

Problem: Return the sorted factors of n while testing only up to √n.

Example: factors(36) gives [1, 2, 3, 4, 6, 9, 12, 18, 36].

Hint

If d divides n, then n // d also divides n. Collect both in a set to avoid duplicating the square root of a perfect square.

Solution
import math

def factors(n):
    found = set()
    for d in range(1, math.isqrt(n) + 1):
        if n % d == 0:
            found.add(d)
            found.add(n // d)
    return sorted(found)

print(factors(36))
print(factors(45))
print(factors(1))

Output:

[1, 2, 3, 4, 6, 9, 12, 18, 36]
[1, 3, 5, 9, 15, 45]
[1]

Time complexity drops from O(n) to O(√n). The set matters for 36, where 6 * 6 would otherwise list 6 twice.

Intermediate Exercises

Exercise I1: Floyd’s Triangle

Problem: Print Floyd’s triangle with n rows.

Example (n = 5): rows 1, 2 3, 4 5 6, 7 8 9 10, 11 12 13 14 15.

Hint

Row i has i numbers. Row i starts right after the last number of row i - 1, which is i * (i - 1) // 2.

Solution
n = 5
for i in range(1, n + 1):
    start = i * (i - 1) // 2 + 1
    print(*range(start, start + i))

Output:

1
2 3
4 5 6
7 8 9 10
11 12 13 14 15

This version replaces the running counter by a formula (the number of items printed before the row is 1 + 2 + ... + (i - 1)). Both approaches work; the counter is easier to read, the formula avoids shared state.

Exercise I2: Primes with the Sieve of Eratosthenes

Problem: List all primes up to N without testing each number separately.

Example: N = 30 gives [2, 3, 5, 7, 11, 13, 17, 19, 23, 29].

Hint

Start with a list of True flags. For each prime p, cross out its multiples starting at p * p.

Solution
def primes_up_to(limit):
    if limit < 2:
        return []
    is_prime = [True] * (limit + 1)
    is_prime[0] = is_prime[1] = False
    p = 2
    while p * p <= limit:
        if is_prime[p]:
            for multiple in range(p * p, limit + 1, p):
                is_prime[multiple] = False
        p += 1
    return [n for n, flag in enumerate(is_prime) if flag]

print(primes_up_to(30))
print(primes_up_to(1))

Output:

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
[]

Multiples smaller than p * p were already crossed out by smaller primes. Time complexity is about O(n log log n), much faster than trial division when many numbers are checked.

Exercise I3: Perfect, Abundant, or Deficient

Problem: Add up the proper divisors of n (divisors smaller than n). If the sum equals n the number is perfect; if greater, abundant; if smaller, deficient.

Example: 6 is perfect (1 + 2 + 3), 12 is abundant (16), 34 is deficient (20).

Hint

range(1, n) excludes n, which is exactly the proper-divisor range.

Solution
def classify(n):
    total = sum(d for d in range(1, n) if n % d == 0)
    if total == n:
        return "perfect"
    return "abundant" if total > n else "deficient"

for n in (6, 12, 28, 34):
    print(n, classify(n))

Output:

6 perfect
12 abundant
28 perfect
34 deficient

For n = 1 the sum is 0, so 1 is deficient. Time is O(n) per number.

Exercise I4: Pronic Numbers

Problem: A pronic number is the product of two consecutive integers, such as 7 * 8 = 56. List the pronic numbers from 1 to 100.

Hint

If n = k * (k + 1), then k * k <= n < (k + 1) * (k + 1), so k = math.isqrt(n).

Solution
import math

def is_pronic(n):
    k = math.isqrt(n)
    return k * (k + 1) == n

print([n for n in range(1, 101) if is_pronic(n)])
print(is_pronic(56), is_pronic(57))

Output:

[2, 6, 12, 20, 30, 42, 56, 72, 90]
True False

Computing k directly avoids a loop and makes each check O(1). Mathematically 0 = 0 * 1 is also pronic; the range starts at 1 here.

Exercise I5: Strong (Krishnamurthy) Numbers

Problem: A number is strong when it equals the sum of the factorials of its digits (for example 145 = 1! + 4! + 5!). Find all strong numbers below 50,000.

Hint

Peel digits with % 10 and // 10 and add math.factorial(digit). Precompute factorials of 0-9 for speed.

Solution
import math

FACT = [math.factorial(d) for d in range(10)]

def is_strong(n):
    total, m = 0, n
    while m:
        total += FACT[m % 10]
        m //= 10
    return total == n

print([n for n in range(1, 50000) if is_strong(n)])
print(is_strong(145))

Output:

[1, 2, 145, 40585]
True

The results are 1, 2, 145, and 40585. A frequent testing mistake is trusting one printed answer: always check a known strong number such as 145 and a known non-member.

Exercise I6: Arithmetic Series with a Formula Check

Problem: Compute 9 + 13 + 17 + ... for N terms with a loop, and check it against the closed form.

Example: N = 20 gives 940.

Hint

The closed form is N * (2 * first + (N - 1) * step) // 2.

Solution
def loop_sum(first, step, n):
    total, term = 0, first
    for _ in range(n):
        total += term
        term += step
    return total

def formula_sum(first, step, n):
    return n * (2 * first + (n - 1) * step) // 2

for n in (1, 5, 20):
    a, b = loop_sum(9, 4, n), formula_sum(9, 4, n)
    print(n, a, b, a == b)

Output:

1 9 9 True
5 85 85 True
20 940 940 True

Comparing two independent methods is a strong way to catch off-by-one mistakes. The loop is O(N), the formula O(1).

Exercise I7: Geometric and Alternating Series

Problem: Compute (a) 5 + 25 + 125 + ... for 10 terms, and (b) the alternating series 2 - 6 + 18 - 54 + ... for 20 terms.

Hint

For (a) the ratio is 5. For (b) the ratio is -3; negative ratios flip the sign automatically.

Solution
def geometric_sum(first, ratio, terms):
    total, term = 0, first
    for _ in range(terms):
        total += term
        term *= ratio
    return total

print(geometric_sum(5, 5, 10))
print(geometric_sum(2, -3, 20))
print(5 * (5 ** 10 - 1) // 4)             # formula check for (a)
print(2 * (1 - (-3) ** 20) // (1 + 3))    # formula check for (b)

Output:

12207030
-1743392200
12207030
-1743392200

Treating the alternating series as a geometric series with ratio -3 removes the need for an if on even and odd positions.

Exercise I8: Triangles from a Word

Problem: For the word CODER, print (a) one triangle where row i is the first i + 1 letters, and (b) a triangle where row i repeats the i-th letter i + 1 times.

Hint

Use slicing for (a): word[:i + 1]. For (b) repeat a one-letter list.

Solution
word = "CODER"
n = len(word)

for i in range(n):
    print(" ".join(word[:i + 1]))

print()

for i in range(n):
    print(" ".join([word[i]] * (i + 1)))

Output:

C
C O
C O D
C O D E
C O D E R

C
O O
D D D
E E E E
R R R R R

" ".join("COD") treats the string as a sequence of characters. Using len(word) keeps the code working for any word.

Exercise I9: Odd-Number Pyramid

Problem: Print a pyramid where row i contains 1, 2, ..., 2i - 1.

Example (n = 5):

        1
      1 2 3
    1 2 3 4 5
  1 2 3 4 5 6 7
1 2 3 4 5 6 7 8 9
Hint

Indent by 2 * (n - i) spaces, then print the numbers 1 to 2i - 1.

Solution
n = 5
for i in range(1, n + 1):
    indent = " " * (2 * (n - i))
    print(indent + " ".join(str(k) for k in range(1, 2 * i)))

Output:

        1
      1 2 3
    1 2 3 4 5
  1 2 3 4 5 6 7
1 2 3 4 5 6 7 8 9

range(1, 2 * i) stops at 2i - 1. str(k) is required because join accepts only strings; for numbers above 9 the alignment shifts, which is a good extension challenge.

Exercise I10: Right-Aligned Countdown Triangles

Problem: Print these right-aligned triangles for n = 5: (a) 54321, 4321, 321, 21, 1; (b) EDCBA, EDCB, EDC, ED, E; (c) EDCBA, DCBA, CBA, BA, A; (d) the word CODER reversed, losing its first letter on each row (REDOC, EDOC, DOC, OC, C).

Hint

Every row is either a slice from the end or a slice from the start of one sequence, indented by 2 * i spaces. Write one helper and feed it different sequences.

Solution
def right_aligned(sequence, keep="end"):
    n = len(sequence)
    for i in range(n):
        part = sequence[i:] if keep == "end" else sequence[:n - i]
        print(" " * (2 * i) + " ".join(part))

right_aligned("54321")
print()
right_aligned("EDCBA", keep="start")
print()
right_aligned("EDCBA")
print()
right_aligned("CODER"[::-1])

Output:

5 4 3 2 1
  4 3 2 1
    3 2 1
      2 1
        1

E D C B A
  E D C B
    E D C
      E D
        E

E D C B A
  D C B A
    C B A
      B A
        A

R E D O C
  E D O C
    D O C
      O C
        C

One function covers four patterns because they differ only in the sequence and in which end the slice keeps. Strings are sequences, so " ".join(part) works on slices of them directly. For numbers above 9 build a list of strings instead of a digit string.

Advanced Exercises

Exercise A1: Sort Records with a Lambda Key

Problem: Sort students by score from highest to lowest; break ties alphabetically by name.

Example: [("Omar", 14), ("Aya", 17), ("Lina", 14)] gives [('Aya', 17), ('Lina', 14), ('Omar', 14)].

Hint

A key can return a tuple. Negate the score to sort it descending.

Solution
students = [("Omar", 14), ("Aya", 17), ("Lina", 14)]
ranked = sorted(students, key=lambda s: (-s[1], s[0]))
print(ranked)

Output:

[('Aya', 17), ('Lina', 14), ('Omar', 14)]

Tuples compare element by element, so the name only matters when scores tie. Negation works for numbers; for descending strings use reverse=True in two passes.

Exercise A2: Euclid’s GCD, LCM, and Step Count

Problem: Write a recursive Euclid GCD, then use it to compute the least common multiple. Count how many calls the subtraction method and Euclid’s method need for (235, 875).

Hint

gcd(a, b) = gcd(b, a % b), with base case b == 0. And lcm(a, b) = a * b // gcd(a, b).

Solution
def gcd_euclid(a, b, calls=1):
    if b == 0:
        return a, calls
    return gcd_euclid(b, a % b, calls + 1)

def gcd_subtract(a, b, calls=1):
    if a == 0 or a == b:
        return b, calls
    if b == 0:
        return a, calls
    if a > b:
        return gcd_subtract(a - b, b, calls + 1)
    return gcd_subtract(a, b - a, calls + 1)

g1, c1 = gcd_euclid(235, 875)
g2, c2 = gcd_subtract(235, 875)
print("Euclid:", g1, "in", c1, "calls")
print("Subtraction:", g2, "in", c2, "calls")
print("LCM:", 235 * 875 // g1)

Output:

Euclid: 5 in 10 calls
Subtraction: 5 in 12 calls
LCM: 41125

Both give 5, but Euclid’s remainder step collapses many subtractions into one. Complexity is O(log min(a, b)) for Euclid versus up to O(max / min) for subtraction.

Exercise A3: Palindromic Letter Pyramids

Problem: Print (a) the pyramid A, ABA, ABCBA, … and (b) the mirrored pyramid E, DED, CDEDC, BCDEDCB, ABCDEDCBA, both for n = 5.

Hint

For (a), row i is the letters A..i followed by the letters i-1..A. For (b), row i starts i letters earlier than E, runs up to E, then back down.

Solution
n = 5
letters = [chr(65 + k) for k in range(n)]      # A..E

for i in range(n):                              # (a)
    row = letters[:i + 1] + letters[:i][::-1]
    print(" " * (2 * (n - 1 - i)) + " ".join(row))

print()

for i in range(n):                              # (b)
    start = n - 1 - i
    row = letters[start:] + letters[start:n - 1][::-1]
    print(" " * (2 * (n - 1 - i)) + " ".join(row))

Output:

        A
      A B A
    A B C B A
  A B C D C B A
A B C D E D C B A

        E
      D E D
    C D E D C
  B C D E D C B
A B C D E D C B A

Part (a) is an ascending slice plus the reverse of a shorter slice. Part (b) reuses the same idea with a slice that grows from the end. Printing the slices on paper first makes the indexes obvious.

Exercise A4: Number Diamond

Problem: Print a diamond where row k repeats the number k exactly 2k - 1 times, growing to n and shrinking back to 1.

Example (n = 3):

    1
  2 2 2
3 3 3 3 3
  2 2 2
    1
Hint

Build the row numbers 1, 2, ..., n, n-1, ..., 1, then print each row with indent 2 * (n - k).

Solution
def number_diamond(n):
    rows = list(range(1, n + 1)) + list(range(n - 1, 0, -1))
    for k in rows:
        print(" " * (2 * (n - k)) + " ".join([str(k)] * (2 * k - 1)))

number_diamond(3)
print()
number_diamond(5)

Output:

    1
  2 2 2
3 3 3 3 3
  2 2 2
    1

        1
      2 2 2
    3 3 3 3 3
  4 4 4 4 4 4 4
5 5 5 5 5 5 5 5 5
  4 4 4 4 4 4 4
    3 3 3 3 3
      2 2 2
        1

Describing the diamond as one list of row values avoids two separate loops. The [str(k)] * (2k - 1) form also works when k has two digits, unlike joining a repeated string.

Exercise A5: Decode a Numbered String

Problem: Using the encoding from the strings section, write a decode function that rejects invalid input, and prove it works with a round trip.

Hint

Round trip: decode(encode(text)) == text for any text made of letters and spaces. Test many strings automatically.

Solution
import random
import string

def encode(text):
    return "".join(str(ord(ch)) for ch in text)[::-1]

def decode(encoded):
    digits = encoded[::-1]
    out, i = [], 0
    while i < len(digits):
        two = int(digits[i:i + 2])
        if two == 32 or 65 <= two <= 90 or 97 <= two <= 99:
            out.append(chr(two))
            i += 2
        else:
            three = int(digits[i:i + 3])
            if not 100 <= three <= 122:
                raise ValueError("invalid encoded string")
            out.append(chr(three))
            i += 3
    return "".join(out)

random.seed(1)
alphabet = string.ascii_letters + " "
for _ in range(1000):
    text = "".join(random.choice(alphabet) for _ in range(random.randint(0, 20)))
    assert decode(encode(text)) == text
print("1000 random round trips passed")

try:
    decode("999")
except ValueError as error:
    print("ValueError:", error)

Output:

1000 random round trips passed
ValueError: invalid encoded string

A randomized round-trip test checks thousands of inputs in milliseconds, far more than manual examples. The assert stops the program on the first failure.

Exercise A6: A Complete Vector Class

Problem: Extend the Vector class with subtraction, equality, absolute value (length), and a readable repr.

Example: Vector(3, 4) has abs equal to 5.0, and Vector(5, 7) - Vector(2, 3) equals Vector(3, 4).

Hint

Use __sub__, __eq__, and __abs__. In __eq__, return NotImplemented for other types.

Solution
import math

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

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, k):
        return Vector(self.x * k, self.y * k)

    __rmul__ = __mul__

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

print(abs(Vector(3, 4)))
print(Vector(5, 7) - Vector(2, 3))
print(Vector(5, 7) - Vector(2, 3) == Vector(3, 4))
print(Vector(1, 1) == "not a vector")
print(2 * Vector(1, 2))

Output:

5.0
Vector(3, 4)
True
False
Vector(2, 4)

Returning NotImplemented lets Python try the other operand or fall back to False, instead of crashing. Defining __eq__ without __hash__ makes instances unhashable, which is acceptable for mutable objects.

Exercise A7: Cooperative Multiple Inheritance

Problem: Build classes Base, Logging, Validation, and Service(Logging, Validation), where each class adds a step to process(data) through super(). Show the order of the steps.

Hint

Each class appends its label and calls super().process(...). The Base class ends the chain.

Solution
class Base:
    def process(self, steps):
        steps.append("Base")
        return steps

class Logging(Base):
    def process(self, steps):
        steps.append("Logging")
        return super().process(steps)

class Validation(Base):
    def process(self, steps):
        steps.append("Validation")
        return super().process(steps)

class Service(Logging, Validation):
    def process(self, steps):
        steps.append("Service")
        return super().process(steps)

print(Service().process([]))
print([c.__name__ for c in Service.__mro__])

Output:

['Service', 'Logging', 'Validation', 'Base']
['Service', 'Logging', 'Validation', 'Base', 'object']

The order matches the MRO: Service, Logging, Validation, Base. Every class in the chain must call super() for cooperative inheritance to work; if one forgets, later classes are skipped.

Exercise A8: Fraction Series

Problem: Compute (a) 2/10 + 4/9 + 6/8 + ... + 20/1, and (b) (x+2)/10 + (x+4)/30 + (x+6)/90 + ... for N terms with x = 2.

Hint

For (a), term i is 2i / (11 - i). For (b), term i is (x + 2i) / (10 * 3 ** (i - 1)).

Solution
total_a = sum((2 * i) / (11 - i) for i in range(1, 11))
print(f"(a) {total_a:.6f}")

def series_b(x, n):
    return sum((x + 2 * i) / (10 * 3 ** (i - 1)) for i in range(1, n + 1))

print(f"(b) {series_b(2, 10):.6f}")

Output:

(a) 44.437302
(b) 0.749936

Deriving each term from i with a formula is less error-prone than updating several running variables. Printing with a fixed number of decimals avoids floating-point noise in the last digits.

Exercise A9: Automorphic Numbers

Problem: A number is automorphic when its square ends with the number itself (25 * 25 = 625). Find all automorphic numbers below 1000.

Hint

Compare n * n % 10 ** len(str(n)) with n.

Solution
def is_automorphic(n):
    digits = len(str(n))
    return n * n % 10 ** digits == n

print([n for n in range(1, 1000) if is_automorphic(n)])
print(is_automorphic(9))

Output:

[1, 5, 6, 25, 76, 376, 625]
False

Taking the square modulo 10 ** digits keeps only the last digits digits of the square. The same result can be reached by comparing digits one at a time with % 10 and // 10.

Exercise A10: Pyramid That Continues the Alphabet

Problem: Print a centered pyramid where row i holds 2i + 1 consecutive letters and the alphabet continues from one row to the next (A, then BCD, then EFGHI, …), for n = 5.

Hint

Keep a counter outside the outer loop, as in Floyd’s triangle, and use chr(65 + counter % 26).

Solution
n = 5
counter = 0
for i in range(n):
    row = []
    for _ in range(2 * i + 1):
        row.append(chr(65 + counter % 26))
        counter += 1
    print(" " * (2 * (n - 1 - i)) + " ".join(row))

Output:

        A
      B C D
    E F G H I
  J K L M N O P
Q R S T U V W X Y

The five rows use 1 + 3 + 5 + 7 + 9 = 25 letters, so the pyramid ends at Y. The % 26 keeps larger pyramids valid by wrapping around to A.

Summary and Next Steps

You have worked through the core toolkit: reading and converting input, printing with precision, conditions and loops, functions with default, variable-length, and keyword arguments, lambdas, recursion, classes with multiple inheritance and operator overloading, and the problem-solving habits behind number problems, series, and patterns.

The most transferable ideas are these:

  • Translate definitions into code. Factors, primes, and series all start from a precise definition.
  • Find the formula for term, row, or column i. Then the loop writes itself.
  • Test the edges. Zero, one, empty input, and large values break many correct-looking solutions.
  • Cross-check with a second method. A formula against a loop, or a round-trip test, catches silent mistakes.

From here, natural next steps are file handling and exceptions, generators and decorators, and then NumPy and pandas, where these same loops are replaced by vectorized operations. Keep the Python documentation close and practice a little every day.

  • python
  • python tutorial
  • functions
  • object-oriented programming
  • recursion
  • pattern printing
  • data science
  • exercises

Share

Portrait of Nbark Kazdabi

Written by Nbark Kazdabi | Xray 0xFF

Master’s Student in Artificial Intelligence and Data Science

I am a Master’s student in Artificial Intelligence and Data Science with a background in Mathematics and Computer Science. Through this blog, I share practical tutorials, technical explanations, and lessons from my learning journey.

About the authorGitHubLinkedIn