back

python workout: exercise 13

printing tuple records

problem

For example, assume we’re in charge of an international summit in London. We know how many hours it’ll take each of several world leaders to arrive:

PEOPLE = [('Donald', 'Trump', 7.85),
          ('Vladimir', 'Putin', 3.626),
          ('Jinping', 'Xi', 10.603)]

The planner for this summit needs to have a list of the world leaders who are coming, along with the time it’ll take for them to arrive. However, this travel planner doesn’t need the degree of precision that the computer has provided; it’s enough for us to have two digits after the decimal point.

For this exercise, write a Python function, format_sort_records, that takes the PEOPLE list and returns a formatted string that looks like the following:

Trump     Donald     7.85
Putin     Vladimir   3.63
Xi        Jinping   10.60

Notice that the last name is printed before the first name (taking into account that Chinese names are generally shown that way), followed by a decimal-aligned indication of how long it’ll take for each leader to arrive in London. Each name should be printed in a 10-character field, and the time should be printed in a 5-character field, with one space character of padding between each of the columns. Travel time should display only two digits after the decimal point, which means that even though the input for Xi Jinping’s flight is 10.603 hours, the value displayed should be 10.60.

attempts

This should be straightforward.

I just need to remember how to format x-character fields; I remember doing something similar in David Beazley’s practical-python.

But a quick look at the docs gives us what we need.

So it should look something like:

PEOPLE = [('Donald', 'Trump', 7.85),
          ('Vladimir', 'Putin', 3.626),
          ('Jinping', 'Xi', 10.603)]

def format_sort_records(records: list[tuple]) -> str:
    formatted_string = []
    for first_name, last_name, eta in records:
        formatted_string.append(f"{last_name:<10} {first_name:<10} {eta:<5.2f}")
    return "\n".join(formatted_string)

print(format_sort_records(PEOPLE))
Trump      Donald     7.85
Putin      Vladimir   3.63
Xi         Jinping    10.60

We use the list approach to string construction because of the immutability of strings in Python.

And looking back at it now, we can just do this in a single line:

PEOPLE = [('Donald', 'Trump', 7.85),
          ('Vladimir', 'Putin', 3.626),
          ('Jinping', 'Xi', 10.603)]

def format_sort_records(records: list[tuple]) -> str:
    return "\n".join(
        f"{last_name:<10} {first_name:<10} {eta:<5.2f}"
        for first_name, last_name, eta in records
    )
print(format_sort_records(PEOPLE))
Trump      Donald     7.85
Putin      Vladimir   3.63
Xi         Jinping    10.60

I think I might even prefer this…

correction

Reading the start of the book’s solution, it seems we had to sort the names alphabetically according to last name and first name.

I suspected as much given the inclusion of “sort” in the name of function we had to implement. But it wasn’t clear to me.

Taking that into account, we can update/correct our final one line solution by leveraging sorted and operator.itemgetter as we did before:

from operator import itemgetter

PEOPLE = [('Donald', 'Trump', 7.85),
          ('Vladimir', 'Putin', 3.626),
          ('Jinping', 'Xi', 10.603)]

def format_sort_records(records: list[tuple]) -> str:
    return "\n".join(
        f"{last_name:<10} {first_name:<10} {eta:<5.2f}"
        for first_name, last_name, eta in sorted(records, key=itemgetter(1,0))
    )
print(format_sort_records(PEOPLE))
Putin      Vladimir   3.63
Trump      Donald     7.85
Xi         Jinping    10.60

But what’s confusing is that this doesn’t match the example in the book’s problem statement:

Trump      Donald     7.85
Putin      Vladimir   3.63
Xi         Jinping    10.60

And at the same time, the book’s formatted string isn’t sorted by last name or first name?

solution

The book’s implementation:

import operator

PEOPLE = [('Donald', 'Trump', 7.85),
          ('Vladimir', 'Putin', 3.626),
          ('Jinping', 'Xi', 10.603)]

def format_sort_records(list_of_tuples):
    output = []
    template = '{1:10} {0:10} {2:5.2f}'
    for person in sorted(list_of_tuples,
                         key=operator.itemgetter(1, 0)):
        output.append(template.format(*person))
    return output

print('\n'.join(format_sort_records(PEOPLE)))
Putin      Vladimir    3.63
Trump      Donald      7.85
Xi         Jinping    10.60

I like the use of a template string and str.format instead of f-strings. I think I might even prefer it.

But I don’t understand why it returns a list of strings instead of a singular formatted string as stated in the problem statement?

I also didn’t notice that the times were meant to be aligned right. But that’s a small fix.

beyond the exercise

namedtuples

  • problem

    Reimplement this exercise using namedtuple objects (http://mng.bz/gyWl), defined in the collections module. Many people like to use named tuples because they give the right balance between readability and efficiency.

  • attempts

    Looking at the docs, we’d start by creating a Person namedtuple before instantiating our example PEOPLE data with it. It should then just be a question of changing the arguments to itemgetter.

    And all this should work well building on top of the book’s implementation:

    from operator import itemgetter
    from collections import namedtuple
    
    Person = namedtuple('Person', ['first_name', 'last_name', 'eta'])
    
    PEOPLE = [Person('Donald', 'Trump', 7.85),
              Person('Vladimir', 'Putin', 3.626),
              Person('Jinping', 'Xi', 10.603)]
    
    def format_sort_people(people: list[Person]) -> str:
        template = '{1:10} {0:10} {2:5.2f}'
        return '\n'.join(
            template.format(*person)
            for person in sorted(people, key=itemgetter(1,0))
        )
    
    print(format_sort_people(PEOPLE))
    
    Putin      Vladimir    3.63
    Trump      Donald      7.85
    Xi         Jinping    10.60
    

    The only trouble is that we don’t make use of the named fields with itemgetter. Unless I’m missing something?

    We could replace it with a lambda to give:

    from collections import namedtuple
    
    Person = namedtuple('Person', ['first_name', 'last_name', 'eta'])
    
    PEOPLE = [Person('Donald', 'Trump', 7.85),
              Person('Vladimir', 'Putin', 3.626),
              Person('Jinping', 'Xi', 10.603)]
    
    def format_sort_people(people: list[Person]) -> str:
        template = '{1:10} {0:10} {2:5.2f}'
        return '\n'.join(
            template.format(*person)
            for person in sorted(people, key=lambda p: (p.last_name, p.first_name))
        )
    
    print(format_sort_people(PEOPLE))
    
    Putin      Vladimir    3.63
    Trump      Donald      7.85
    Xi         Jinping    10.60
    

    Actually, operator.attrgetter might do the job:

    from operator import attrgetter
    from collections import namedtuple
    
    Person = namedtuple('Person', ['first_name', 'last_name', 'eta'])
    
    PEOPLE = [Person('Donald', 'Trump', 7.85),
              Person('Vladimir', 'Putin', 3.626),
              Person('Jinping', 'Xi', 10.603)]
    
    def format_sort_people(people: list[Person]) -> str:
        template = '{1:10} {0:10} {2:5.2f}'
        return '\n'.join(
            template.format(*person)
            for person in sorted(people, key=attrgetter('last_name', 'first_name'))
        )
    
    print(format_sort_people(PEOPLE))
    
    Putin      Vladimir    3.63
    Trump      Donald      7.85
    Xi         Jinping    10.60
    

    And it does!

    Very nice!

oscar nominees

  • problem

    Define a list of tuples, in which each tuple contains the name, length (in min- utes), and director of the movies nominated for best picture Oscar awards last year. Ask the user whether they want to sort the list by title, length, or director’s name, and then present the list sorted by the user’s choice of axis.

  • attempts

    Let’s reuse namedtuple for this. It’s a nice, organised way of representing our data.

    Which, after a quick search, looks like this for the 2025 Best Picture Oscar nominees:

    from collections import namedtuple
    
    Film = namedtuple('Film', ['name', 'length', 'director'])
    
    NOMINEES = [
        Film("I'm still here", 138, 'Walter Salles'),
        Film('Conclave', 120, 'Edward Berger'),
        Film('Dune: Part Two', 167, 'Denis Villeneuve'),
        Film('Anora', 139, 'Sean Baker'),
        Film('The Substance', 141, 'Coralie Fargeat'),
        Film('Wicked', 162, 'Jon M. Chu'),
        Film('The Brutalist', 215, 'Brady Corbet'),
        Film('A Complete Unknown', 140, 'James Mangold'),
        Film('Nickel Boys', 140, 'RaMell Ross'),
        Film('Emilia Perez', 132, 'Jacques Audiard')
    ]
    

    To control the sorted order, we’ll just take an additional argument. It will be the attribute name of the named tuple the user wants to sort by. And for the sake of simplicity, we want perform any validation/checks on whether the attribute name is correct. We’ll just let things fail:

    from collections import namedtuple
    from operator import attrgetter
    
    Film = namedtuple('Film', ['title', 'duration', 'director'])
    
    NOMINEES = [
        Film("I'm still here", 138, 'Walter Salles'), Film('Conclave', 120, 'Edward Berger'),
        Film('Dune: Part Two', 167, 'Denis Villeneuve'), Film('Anora', 139, 'Sean Baker'),
        Film('The Substance', 141, 'Coralie Fargeat'), Film('Wicked', 162, 'Jon M. Chu'),
        Film('The Brutalist', 215, 'Brady Corbet'), Film('A Complete Unknown', 140, 'James Mangold'),
        Film('Nickel Boys', 140, 'RaMell Ross'), Film('Emilia Perez', 132, 'Jacques Audiard')
    ]
    
    def format_sort_films(films: list[Film], sort_by: str) -> str:
        template = '{0:20} {2:20} {1:5}'
        return '\n'.join(
            template.format(*film)
            for film in sorted(films, key=attrgetter(sort_by))
        )
    
    print(format_sort_films(NOMINEES, 'title'))
    print()
    print(format_sort_films(NOMINEES, 'duration'))
    print()
    print(format_sort_films(NOMINEES, 'director'))
    
    A Complete Unknown   James Mangold          140
    Anora                Sean Baker             139
    Conclave             Edward Berger          120
    Dune: Part Two       Denis Villeneuve       167
    Emilia Perez         Jacques Audiard        132
    I'm still here       Walter Salles          138
    Nickel Boys          RaMell Ross            140
    The Brutalist        Brady Corbet           215
    The Substance        Coralie Fargeat        141
    Wicked               Jon M. Chu             162
    
    Conclave             Edward Berger          120
    Emilia Perez         Jacques Audiard        132
    I'm still here       Walter Salles          138
    Anora                Sean Baker             139
    A Complete Unknown   James Mangold          140
    Nickel Boys          RaMell Ross            140
    The Substance        Coralie Fargeat        141
    Wicked               Jon M. Chu             162
    Dune: Part Two       Denis Villeneuve       167
    The Brutalist        Brady Corbet           215
    
    The Brutalist        Brady Corbet           215
    The Substance        Coralie Fargeat        141
    Dune: Part Two       Denis Villeneuve       167
    Conclave             Edward Berger          120
    Emilia Perez         Jacques Audiard        132
    A Complete Unknown   James Mangold          140
    Wicked               Jon M. Chu             162
    Nickel Boys          RaMell Ross            140
    Anora                Sean Baker             139
    I'm still here       Walter Salles          138
    

user enabled sort

  • problem

    Extend this exercise by allowing the user to sort by two or three of these fields, not just one of them. The user can specify the fields by entering them separated by commas; you can use str.split to turn them into a list.

  • attempts

    We’ve been sorting by multiple fields from the start.

    This exercise extension just requires us to use input and pass the results of str.split() into itemgetter or attrgetter depending on whether we’re using regular tuples or namedtuple.

    If we build off the first extension exercise we should get something like:

    from operator import attrgetter
    from collections import namedtuple
    
    Person = namedtuple('Person', ['first_name', 'last_name', 'eta'])
    
    PEOPLE = [Person('Donald', 'Trump', 7.85),
              Person('Vladimir', 'Putin', 3.626),
              Person('Jinping', 'Xi', 10.603)]
    
    def format_sort_people(people: list[Person]) -> str:
        template = '{1:10} {0:10} {2:5.2f}'
        print(f'Available fields to sort by: {Person._fields}')
        sortby = input(f"Sort by field(s): ").split(',')
        return '\n'.join(
            template.format(*person)
            for person in sorted(people, key=attrgetter(*sortby))
        )
    
    print(format_sort_people(PEOPLE))
    
    Available fields to sort by: ('first_name', 'last_name', 'eta')
    Sort by field(s): eta,last_name
    Putin      Vladimir    3.63
    Trump      Donald      7.85
    Xi         Jinping    10.60
    

    It’s not perfect. But it does the job.

mail@jonahv.com