Skip to content
Laviyx Learn

Try

Advanced Python Practice

Practise comprehensions, iterators, generators, decorators and context managers, the features that make Python code short and expressive.

Advanced9 problemsUpdated

Review first: Comprehensions, Iterators, Generators, Decorators, Context Managers

  1. 1.List, dict and set comprehensions

    Advanced

    Given words = ["apple", "Banana", "cherry", "date", "Elderberry", "fig"], build with comprehensions:

    1. a list of the words with more than 4 letters, in lower case
    2. a dictionary mapping each word to its length
    3. the sorted set of first letters, in lower case
    Show hint

    [expr for x in items if condition], {key: value for x in items} and {expr for x in items}.

    Show solution
    Python
    words = ["apple", "Banana", "cherry", "date", "Elderberry", "fig"]
    
    long_words = [w.lower() for w in words if len(w) > 4]
    lengths = {w: len(w) for w in words}
    first_letters = sorted({w[0].lower() for w in words})
    
    print(long_words)
    print(lengths)
    print(first_letters)
    Output
    ['apple', 'banana', 'cherry', 'elderberry']
    {'apple': 5, 'Banana': 6, 'cherry': 6, 'date': 4, 'Elderberry': 10, 'fig': 3}
    ['a', 'b', 'c', 'd', 'e', 'f']
  2. 2.Flatten and transpose a matrix

    Advanced

    Given matrix = [[1, 2, 3], [4, 5, 6]], use comprehensions to flatten it into one list, and to transpose it (turn rows into columns).

    Show hint

    In a nested comprehension the for clauses read in the same order as nested loops: [x for row in matrix for x in row].

    Show solution
    Python
    matrix = [[1, 2, 3], [4, 5, 6]]
    
    flat = [x for row in matrix for x in row]
    transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
    
    print(flat)
    print(transposed)
    Output
    [1, 2, 3, 4, 5, 6]
    [[1, 4], [2, 5], [3, 6]]

    list(zip(*matrix)) also transposes, giving tuples: [(1, 4), (2, 5), (3, 6)].

  3. 3.Write an iterator class

    Advanced

    Write a Countdown class so that for n in Countdown(3): gives 3, 2, 1. Then show what next() does when the iterator is used up.

    Show hint

    An iterator needs __iter__ (returning self) and __next__, which raises StopIteration when there are no values left.

    Show solution
    Python
    class Countdown:
        def __init__(self, start):
            self.current = start
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.current <= 0:
                raise StopIteration
            value = self.current
            self.current -= 1
            return value
    
    
    for n in Countdown(3):
        print(n)
    
    it = Countdown(1)
    print(next(it))
    try:
        next(it)
    except StopIteration:
        print("No more values")
    Output
    3
    2
    1
    1
    No more values
  4. 4.An endless generator

    Advanced

    Write a generator fibonacci() that yields the Fibonacci numbers forever (0, 1, 1, 2, 3, …). Print the first 10 of them.

    Show hint

    Use yield inside a while True loop. Take a limited number with itertools.islice or zip with range.

    Show solution
    Python
    from itertools import islice
    
    
    def fibonacci():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a + b
    
    
    print(list(islice(fibonacci(), 10)))
    Output
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    A generator produces values only when asked, so an infinite sequence is fine as long as you only take a finite part of it. Calling list(fibonacci()) would never finish.

  5. 5.A lazy pipeline

    Advanced

    Process these log lines with a pipeline of generator expressions: strip each line, skip empty ones, keep only lines containing ERROR, and print them. Then use a generator expression to sum the squares of 1 to 1,000,000 without building a list.

    log = ["INFO start\n", "ERROR disk full\n", "\n", "INFO retry\n", "ERROR timeout\n"]

    Show hint

    Each step can be a generator expression over the previous one: (line.strip() for line in log). sum(x * x for x in range(...)) needs no extra brackets.

    Show solution
    Python
    log = ["INFO start\n", "ERROR disk full\n", "\n", "INFO retry\n", "ERROR timeout\n"]
    
    stripped = (line.strip() for line in log)
    non_empty = (line for line in stripped if line)
    errors = (line for line in non_empty if "ERROR" in line)
    
    for line in errors:
        print(line)
    
    print(sum(x * x for x in range(1, 1_000_001)))
    Output
    ERROR disk full
    ERROR timeout
    333333833333500000

    No intermediate lists are created; each line flows through all three steps one at a time. The same code works on a log file with millions of lines (open("app.log") is itself an iterator over lines).

  6. 6.A decorator that logs calls

    Advanced

    Write a decorator log_calls that prints the function name and arguments before each call and the result after it. Apply it to add(a, b) and call add(2, 3). Make sure add.__name__ is still add.

    Show hint

    The decorator defines an inner wrapper(*args, **kwargs) that calls the original function. @functools.wraps(func) copies the name and docstring.

    Show solution
    Python
    import functools
    
    
    def log_calls(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Calling {func.__name__} with {args} {kwargs}")
            result = func(*args, **kwargs)
            print(f"{func.__name__} returned {result}")
            return result
        return wrapper
    
    
    @log_calls
    def add(a, b):
        return a + b
    
    
    total = add(2, 3)
    print(total)
    print(add.__name__)
    Output
    Calling add with (2, 3) {}
    add returned 5
    5
    add
  7. 7.A decorator with arguments

    Advanced

    Write a decorator repeat(times) that runs the decorated function times times. Use it as @repeat(3) on a function that prints Hello, <name>!.

    Show hint

    A decorator with arguments is a function that returns a decorator, so it has three levels of functions.

    Show solution
    Python
    import functools
    
    
    def repeat(times):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                for _ in range(times):
                    func(*args, **kwargs)
            return wrapper
        return decorator
    
    
    @repeat(3)
    def greet(name):
        print(f"Hello, {name}!")
    
    
    greet("Asha")
    Output
    Hello, Asha!
    Hello, Asha!
    Hello, Asha!

    @repeat(3) first calls repeat(3), which returns decorator; then decorator(greet) returns wrapper, which replaces greet.

  8. 8.A context manager class

    Advanced

    Write a context manager class Section that prints [start] name on entry and [end] name on exit, even if an error happens inside the block. Use it once normally, and once with a ValueError raised inside, catching the error outside the with.

    Show hint

    Implement __enter__ and __exit__(self, exc_type, exc, tb). Returning False (or nothing) from __exit__ lets the exception continue.

    Show solution
    Python
    class Section:
        def __init__(self, name):
            self.name = name
    
        def __enter__(self):
            print(f"[start] {self.name}")
            return self
    
        def __exit__(self, exc_type, exc, tb):
            status = "failed" if exc_type else "ok"
            print(f"[end] {self.name} ({status})")
            return False
    
    
    with Section("load"):
        print("loading data")
    
    try:
        with Section("parse"):
            raise ValueError("bad row")
    except ValueError as error:
        print("Caught:", error)
    Output
    [start] load
    loading data
    [end] load (ok)
    [start] parse
    [end] parse (failed)
    Caught: bad row
  9. 9.A context manager with @contextmanager

    Advanced

    Using contextlib.contextmanager, write temporary_setting(settings, key, value) that changes settings[key] inside the with block and restores the old value afterwards, even if an error occurs.

    Show hint

    Code before yield runs on entry and code after it on exit. Put the restore in a finally block.

    Show solution
    Python
    from contextlib import contextmanager
    
    
    @contextmanager
    def temporary_setting(settings, key, value):
        old = settings[key]
        settings[key] = value
        try:
            yield settings
        finally:
            settings[key] = old
    
    
    settings = {"debug": False}
    
    with temporary_setting(settings, "debug", True):
        print("inside:", settings)
    
    print("after:", settings)
    Output
    inside: {'debug': True}
    after: {'debug': False}