We closed the previous lesson with EasyTask calculating remaining days, progress percentages and urgencies, but with two very visible shortcomings: the card was printed at a rate of one line per item, illegible, and all the data was still written inside the code. To register the task of another client you had to open the editor.

This lesson fixes both things, and with that your program crosses an important frontier: it stops being something that gets edited and becomes something that gets used. A program that gets used has two points of contact with the world: the input (the data it receives) and the output (the results it displays). In Python those two points are called input() and print().

You have known print() since module 1, but only in its simplest form. Here you will see it in depth: several arguments, the sep and end parameters, escape characters, and above all f-strings, the tool with which text is composed in modern Python and with which you will at last be able to mix words and variables on a single line, line up columns and display decimals to whatever precision you want. Then input() will arrive, and with it a detail that conditions everything that follows: input() always returns text, even when the user types a number.

Contents

  1. Input and output: a program's two points of contact
  2. print() with several arguments
  3. The sep and end parameters
  4. Escape characters: \n, \t and quotes
  5. f-strings: interpolating variables
  6. Expressions inside an f-string
  7. Format specifiers: decimals and alignment
  8. Other ways of formatting that you will see in other people's code
  9. input(): asking for data from the keyboard
  10. The crucial point: input() always returns text
  11. EasyTask: the card is asked for and presented
  12. Common mistakes and tips
  13. Exercises
  14. Conclusion

  1. Input and output: a program's two points of contact

Any program, from the simplest one to a banking system, follows the same three-stage scheme:

flowchart LR
    A["INPUT<br/>data coming in"] --> B["PROCESS<br/>calculations and decisions"]
    B --> C["OUTPUT<br/>results being shown"]

Until now you have worked in the middle: variables (02-01) and expressions (02-02) are pure process. The input was fixed in the code and the output was rudimentary. We are going to cover both ends.

Data can come in by many routes — keyboard, files, network, sensors — and go out by just as many. In this course we start with the most direct: the keyboard to come in and the screen (specifically, the terminal) to go out. Reading and writing files arrives in Saving Data to Files.

A background warning worth bearing in mind from day one: everything that comes in from outside is suspect. The user may type a number where you expected a name, leave the field empty or write "HIGH" in upper case. Checking that the data is correct is called validating, and it is the subject of the next lesson. Here we deal with receiving it and displaying it.

  1. print() with several arguments

So far you have used print() with a single value. But it accepts several, separated by commas:

title = "Sole Bakery logo"
assignee = "Luis"

print("Title:", title)
print("Assignee:", assignee)

Output:

Title: Sole Bakery logo
Assignee: Luis

This solves at a stroke the problem we left pending in the previous lesson: remember that "Title: " + title only worked with text, and that "Days: " + 3 gave a TypeError. With commas there is no problem, because print() converts each value to text automatically:

remaining_days = 7
percentage = 37.5
completed = False
due_date = None

print("Remaining days:", remaining_days)      # Remaining days: 7
print("Percentage:", percentage)              # Percentage: 37.5
print("Completed:", completed)                # Completed: False
print("Delivery:", due_date)                  # Delivery: None

Numbers, booleans, None: print() displays them all without complaint.

There are two details to observe. First: a space appears between each argument, put there by Python, not by you. That is why "Title:" has no trailing space and the output still looks right. Second: there is no limit on the number of arguments.

print("Task", 1, "of", 3, "->", 33.3, "%")
Task 1 of 3 -> 33.3 %

Notice the result: the % appears separated from the number because the automatic space is put between all the arguments. That automatic space is convenient but also rigid; the next section explains how to control it.

And a print() with no arguments prints a blank line:

print()      # empty line

It is cleaner than print(""), which does the same thing.

  1. The sep and end parameters

print() accepts two named parameters that modify its behaviour.

sep changes the separator placed between the arguments. By default it is a space.

print("high", "medium", "low")                # high medium low
print("high", "medium", "low", sep=" / ")     # high / medium / low
print("high", "medium", "low", sep="")        # highmediumlow
print("2026", "08", "04", sep="-")            # 2026-08-04

That last example is the typical use: composing a date or a path without repeating the separator by hand.

end changes what is printed at the end, after the last argument. By default it is a line break, "\n"; that is why every print() starts on a new line.

print("Loading", end="")
print(".", end="")
print(".", end="")
print(".", end="")
print(" done")

Output, all on a single line:

Loading... done

By putting end="" you are telling print() not to break the line, so the next statement carries on where the previous one left off. It is the mechanism behind progress bars and any output built up in parts.

Combining the two:

print("Marta", "Luis", "Nuria", sep=", ", end=".\n")
Marta, Luis, Nuria.
Parameter Default value What it controls
sep " " (a space) What is put between the arguments
end "\n" (line break) What is put after the last one

Named parameters like these are called keyword arguments, and they are studied in depth in Parameters and Return Values. For now it is enough to know that they are written name=value and that they go at the end of the call.

  1. Escape characters: \n, \t and quotes

Inside a text string, the backslash \ has a special meaning: together with the character that follows it, it forms an escape sequence representing something that cannot be typed directly.

Escape Meaning Typical use
\n Line break Several lines in a single print
\t Tab Separating columns
\" Literal double quote Quotes inside text delimited by double quotes
\' Literal single quote The same with single quotes
\\ Literal backslash Windows paths

Line breaks. A single print can produce several lines:

print("TASK CARD\n----------------\nClient: Sole Bakery")
TASK CARD
----------------
Client: Sole Bakery

Tabs. \t advances to the next tab stop, which gives a column effect:

print("Marta\tcoordinator")
print("Luis\tdesign")
print("Nuria\tlayout")
Marta	coordinator
Luis	design
Nuria	layout

It works, but with an important limitation: tab stops are fixed (normally every 8 characters), so if a name is longer than the stop, the column goes out of line. To align properly you use the width specifiers of f-strings, which you will see in section 7.

Quotes inside text. There are three ways of dealing with it:

# 1. Different quotes inside and outside
print("The client said: 'I want it by Friday'")

# 2. Escaping the quotes
print("The client said: \"I want it by Friday\"")

# 3. Single quotes outside, double inside
print('The client said: "I want it by Friday"')

The first and the third are the most readable; the second is useful when the text contains both types of quote.

Literal backslash. Since \ starts an escape, to write a backslash you have to double it:

print("C:\\Users\\marta\\easytask")          # C:\Users\marta\easytask

There is also the raw string, which disables escapes: it is marked with an r in front. It is very handy for Windows paths:

print(r"C:\Users\marta\easytask")            # C:\Users\marta\easytask

And for long, multi-line text, triple quotes preserve the format exactly as you write it:

print("""
=== EASYTASK ===
Task manager
Alba Studio
""")

  1. f-strings: interpolating variables

We come to the central tool of this lesson. An f-string (formatted string) is a string preceded by the letter f in which you can insert variables between curly braces {}.

assignee = "Luis"
remaining_days = 7

print(f"The task belongs to {assignee} and there are {remaining_days} days left.")
The task belongs to Luis and there are 7 days left.

Compare the three ways of doing the same thing:

Form Code Problem
Concatenation "The task belongs to " + assignee Does not accept numbers without converting them
print arguments print("The task belongs to", assignee) Automatic spaces that are hard to control
f-string f"The task belongs to {assignee}" None: it reads like the result

The decisive advantage of the f-string is that the code looks like the result. You see the complete sentence and, inside it, in its exact place, the gap the data will fill. And it works with any type, with no conversions:

title = "Sole Bakery logo"
hours = 9.5
completed = False
date = None

print(f"Task: {title}")               # Task: Sole Bakery logo
print(f"Hours: {hours}")              # Hours: 9.5
print(f"Done: {completed}")           # Done: False
print(f"Delivery: {date}")            # Delivery: None

The f goes right up against the opening quote, and it can be used with single, double or triple quotes. If you forget the f, there is no error: the braces are printed literally, which is a very common slip.

print("There are {remaining_days} days left")     # There are {remaining_days} days left  <- f missing
print(f"There are {remaining_days} days left")    # There are 7 days left

If you need a literal brace inside an f-string, double it: {{ produces {.

  1. Expressions inside an f-string

Inside the braces there is room not only for variables: there is room for any expression of the ones you learned in the previous lesson.

hours_spent = 9
estimated_hours = 24
current_day = 12
due_day = 19
priority = "high"
completed = False

print(f"Progress: {hours_spent / estimated_hours * 100} %")
print(f"{due_day - current_day} days left")
print(f"Hours to go: {estimated_hours - hours_spent}")
print(f"Urgent: {priority == 'high' and not completed}")
Progress: 37.5 %
7 days left
Hours to go: 15
Urgent: True

Notice the last line: inside the braces there is a complete logical expression, and in the f-string single quotes have been used for 'high' because the double ones are already taken by the outer string. Mixing the two types of quote is the way to avoid the clash.

That said, a piece of style advice: the fact that you can put complex expressions in does not mean you should. This line is correct but illegible:

print(f"{(hours_spent / estimated_hours * 100 > 60) and (due_day - current_day < 5)}")

It is far better to calculate beforehand, with a name that explains what it is, and limit the f-string to displaying:

progress_percentage = hours_spent / estimated_hours * 100
remaining_days = due_day - current_day
tight_on_time = progress_percentage > 60 and remaining_days < 5

print(f"Tight on time: {tight_on_time}")

The practical rule: inside an f-string, short and obvious expressions; everything else, in a named variable.

A useful trick for debugging: if you put an = after the expression, the f-string also displays the code that produced it.

hours_spent = 9
print(f"{hours_spent=}")              # hours_spent=9

It is extremely handy when you are hunting for why a value is not what you expected.

  1. Format specifiers: decimals and alignment

Inside the braces you can add, after a colon, a format specifier controlling how the value is presented. The general syntax is {value:format}.

Controlling the decimals

The problem is obvious as soon as you calculate a percentage:

percentage = 37.333333333333336
print(f"Progress: {percentage} %")          # Progress: 37.333333333333336 %
print(f"Progress: {percentage:.2f} %")      # Progress: 37.33 %
print(f"Progress: {percentage:.1f} %")      # Progress: 37.3 %
print(f"Progress: {percentage:.0f} %")      # Progress: 37 %

.2f means "floating-point format with 2 decimals". It rounds, it does not truncate: 37.336 with .2f gives 37.34.

It is the specifier you will use most. Any quantity calculated with / that is going to be shown to a person should carry it.

Lining up columns

This is where f-strings definitively beat the \t tab. You can set a minimum width and an alignment:

Specifier Meaning Example with "Luis"
{text:<10} Aligns to the left within 10 characters Luis______
{text:>10} Aligns to the right within 10 characters ______Luis
{text:^10} Centres within 10 characters ___Luis___
{text:<10} with padding {text:.<10} pads with dots Luis......

(The underscores stand for spaces.)

Let's see it composing a table of the Alba Studio team:

print(f"{'ASSIGNEE':<12}{'TASKS':>8}{'HOURS':>8}")
print("-" * 28)
print(f"{'Marta':<12}{3:>8}{12.5:>8}")
print(f"{'Luis':<12}{5:>8}{27.0:>8}")
print(f"{'Nuria':<12}{2:>8}{9.25:>8}")
ASSIGNEE       TASKS   HOURS
----------------------------
Marta              3    12.5
Luis               5    27.0
Nuria              2    9.25

The text is aligned to the left (<) because that is how words are read; the numbers to the right (>) because that is how units, tens and hundreds line up. It is the convention of any well-made table.

And width and decimals can be combined, putting the width first and the precision afterwards:

print(f"{'Marta':<12}{12.5:>8.2f}")
print(f"{'Luis':<12}{27.0:>8.2f}")
print(f"{'Nuria':<12}{9.25:>8.2f}")
Marta          12.50
Luis           27.00
Nuria           9.25

Now we are talking: a perfect column, with two decimals in every case.

Specifier What it does
{x:.2f} 2 decimals
{x:>8} Width 8, right aligned
{x:<15} Width 15, left aligned
{x:^20} Width 20, centred
{x:>10.2f} Width 10, right aligned, 2 decimals
{x:,} Thousands separator: 12500001,250,000
{x:.1%} As a percentage: 0.37537.5%

That last one deserves a note: {0.375:.1%} multiplies by 100 and adds the symbol. If your value is already on a 0-100 scale, use .1f and write the % yourself.

  1. Other ways of formatting that you will see in other people's code

f-strings have existed since Python 3.6 and are the recommended form today. But you will find code — old tutorials, legacy projects — written with the two earlier forms, and it is worth recognising them.

The .format() method, from Python 3.0 onwards:

assignee = "Luis"
days = 7

print("The task belongs to {} and there are {} days left.".format(assignee, days))
print("The task belongs to {0} and there are {1} days left.".format(assignee, days))
print("Progress: {:.2f} %".format(37.333))

The {} gaps are filled in order with the arguments of .format(). The specifiers after the colon are the same as in f-strings, so what you learned in the previous section serves you just as well.

The % operator, inherited from the C language and still present in a lot of old code:

print("The task belongs to %s and there are %d days left." % (assignee, days))
print("Progress: %.2f %%" % 37.333)

Here %s is a gap for text, %d for an integer and %.2f for a decimal with two figures. It is the most cryptic of the three and the most error-prone.

Form Appearance When to use it
f-string f"Hello {name}" Always, in new code
.format() "Hello {}".format(name) Only if you maintain old code
% "Hello %s" % name Never in new code; recognising it is enough

Take this away with you: write f-strings, and if you come across the other two, you now know what they do.

  1. input(): asking for data from the keyboard

input() is the function that stops the program and waits for the user to type something and press Enter.

assignee = input()
print(assignee)

That program sits waiting without saying anything, which is a dreadful user experience. That is why input() accepts an argument: the prompt message, which is displayed before waiting.

assignee = input("Task assignee: ")
print(f"Task assigned to {assignee}")

A run (what the user types is where the cursor is, marked separately here):

Task assignee: Nuria
Task assigned to Nuria

How it works step by step:

flowchart TD
    A["The program reaches input()"] --> B["It shows the prompt message"]
    B --> C["It STOPS and waits"]
    C --> D["The user types and presses Enter"]
    D --> E["input() returns what was typed<br/>as text"]
    E --> F["That text is assigned to the variable"]

Three points of use that make all the difference:

1. End the prompt with a space, or a colon and a space. Without it, what the user types will appear stuck to the message:

name = input("Assignee:")        # Assignee:Nuria   <- ugly
name = input("Assignee: ")       # Assignee: Nuria  <- correct

2. Say exactly what you expect. A vague prompt produces useless data:

priority = input("Priority: ")                          # the user does not know what to put
priority = input("Priority (high/medium/low): ")        # much better

3. The result has to be stored. Writing input("Name: ") without assigning it to anything asks for the data and throws it away. It is a common slip.

And a consequence you notice when running: input() blocks the program. Nothing else happens until the user presses Enter. If your program seems to have "hung", check whether it is waiting for input.

  1. The crucial point: input() always returns text

Here is the most important idea of the lesson, and it is worth reading twice:

input() ALWAYS returns a value of type str, without exception, even when the user types a number.

Check it:

days = input("Estimated days: ")

print(days)          # 3
print(type(days))    # <class 'str'>   <- NOT an int

The user has typed 3, on screen you see 3, but what is stored is the text "3", not the number 3. And that has immediate and disconcerting consequences:

days = input("Estimated days: ")     # the user types 3

print(days * 2)      # 33     <- it repeats the text, it does not multiply!

"3" * 2 is string repetition, which you already saw in the previous lesson: it produces "33". It is not a flaw in Python; it is that you are multiplying text.

And if you attempt real arithmetic, the program stops:

days = input("Estimated days: ")     # the user types 3

print(days + 1)
TypeError: can only concatenate str (not "int") to str

It is the same TypeError from the previous lesson, seen now from the other side. Let's sum up what happens depending on what you attempt:

Operation with days = input() (user types 3) Result
print(days) Displays 3 — it looks like a number
type(days) <class 'str'> — it is not one
days + 1 TypeError
days * 2 "33" (repetition, not multiplication)
days > 2 TypeError — text cannot be compared with a number
f"Days: {days}" Days: 3 — it works, because it is only being displayed

Notice the last row: displaying the data works perfectly. The problem only appears when you want to calculate with it.

That is why, in this lesson's application, EasyTask will ask only for data that is text by nature — title, assignee, priority — and will not ask for the estimated days or the hours. As soon as a number is needed, we will have to convert it, and that conversion, together with what happens if the user types "three" instead of "3", is exactly the content of Type Conversion and Validation.

So we leave two loose ends noted down:

  • input() returns text and sometimes we need numbers.
  • The user may write "HIGH", " high " or "urgent", and none of the three matches "high" (that discovery from the desk check in 01-05).

Both are tied up in the next lesson.

  1. EasyTask: the card is asked for and presented

Version 0.4 of easytask.py. The program asks from the keyboard for the three text items of the task and displays a card lined up in columns with f-strings. The numeric data is still in the code, waiting until we know how to convert it.

# easytask.py - Alba Studio
# Version 0.4: the text data is asked for from the keyboard
#              and the card is presented with f-strings

APP_NAME = "EasyTask"
VERSION = "0.4"
STUDIO = "Alba Studio"
WIDTH = 46
HOURS_PER_WORKDAY = 8

# --- Header ---
print("=" * WIDTH)
print(f"{APP_NAME + ' v' + VERSION:^{WIDTH}}")
print(f"{'Task manager for ' + STUDIO:^{WIDTH}}")
print("=" * WIDTH)
print()

# --- INPUT: data typed by the user ---
print("Enter the task details:")
print()

title = input("  Title                        : ")
description = input("  Description                  : ")
assignee = input("  Assignee (Marta/Luis/Nuria)  : ")
priority = input("  Priority (high/medium/low)   : ")

# --- Data we cannot ask for yet (they are numbers) ---
current_day = 12
due_day = 19
estimated_hours = 24
hours_spent = 9
completed = False

# --- PROCESS: calculations with the data ---
remaining_days = due_day - current_day
progress_percentage = hours_spent / estimated_hours * 100
remaining_hours = estimated_hours - hours_spent
remaining_workdays = remaining_hours / HOURS_PER_WORKDAY
is_urgent = priority == "high" and not completed

# --- OUTPUT: aligned card ---
print()
print("=" * WIDTH)
print(f"{'TASK CARD':^{WIDTH}}")
print("=" * WIDTH)
print(f"{'Title':<16}{title:>{WIDTH - 16}}")
print(f"{'Assignee':<16}{assignee:>{WIDTH - 16}}")
print(f"{'Priority':<16}{priority:>{WIDTH - 16}}")
print(f"{'Completed':<16}{str(completed):>{WIDTH - 16}}")
print("-" * WIDTH)
print(f"{'Remaining days':<16}{remaining_days:>{WIDTH - 16}}")
print(f"{'Hours done':<16}{hours_spent:>{WIDTH - 16}.1f}")
print(f"{'Total hours':<16}{estimated_hours:>{WIDTH - 16}.1f}")
print(f"{'Progress':<16}{progress_percentage:>{WIDTH - 18}.1f} %")
print(f"{'Workdays left':<16}{remaining_workdays:>{WIDTH - 16}.2f}")
print("-" * WIDTH)
print(f"{'Urgent':<16}{str(is_urgent):>{WIDTH - 16}}")
print("=" * WIDTH)
print()
print(f"Description: {description}")

A complete run, with Marta registering the job for client Vidal:

==============================================
                EasyTask v0.4
         Task manager for Alba Studio
==============================================

Enter the task details:

  Title                        : Vidal business cards
  Description                  : 300 units, double-sided, recycled paper
  Assignee (Marta/Luis/Nuria)  : Nuria
  Priority (high/medium/low)   : high

==============================================
                  TASK CARD
==============================================
Title                     Vidal business cards
Assignee                                 Nuria
Priority                                  high
Completed                                False
----------------------------------------------
Remaining days                               7
Hours done                                 9.0
Total hours                               24.0
Progress                                37.5 %
Workdays left                             1.88
----------------------------------------------
Urgent                                    True
==============================================

Description: 300 units, double-sided, recycled paper

Let's go over the techniques used, because the example condenses the whole lesson:

  • "=" * WIDTH draws the separators. It is the string repetition from the previous lesson, here with a very practical use.
  • {text:^{WIDTH}} centres the title within the total width. Notice the advanced detail: the width is not written by hand, it is another variable between braces. So, by changing WIDTH on the first line, the whole card readjusts.
  • {'Title':<16} reserves 16 characters for the label, aligned to the left.
  • {title:>{WIDTH - 16}} takes up the rest of the line with the value aligned to the right. Inside the width braces there is an expression, WIDTH - 16, perfectly valid.
  • {progress_percentage:>{WIDTH - 18}.1f} % combines width, alignment and one decimal; the - 18 instead of - 16 leaves room for the two characters of the % that follow, so the line still measures exactly WIDTH.
  • str(completed) converts the boolean to text. It is necessary because the < and > alignment specifiers work differently with booleans than with strings; by converting it first, it lines up like any other text. The str() function is, precisely, the first thing you will see in the next lesson.

What still does not work

Try this run and look at the result:

  Assignee (Marta/Luis/Nuria)  : NURIA
  Priority (high/medium/low)   : HIGH

The card comes out just as pretty, but at the bottom it says Urgent False. The comparison priority == "high" has failed because the user wrote in upper case. And if they write superurgent, or leave the field blank by pressing Enter, the program does not complain either: it stores the rubbish and carries on.

That is exactly the work outstanding.

Common Mistakes and Tips

Forgetting the f of the f-string. print("There are {days} days left") prints the braces as they are. It does not raise an error, so you have to spot it in the output. If you see braces on screen, the f is missing.

Using double quotes inside an f-string delimited by double quotes. f"It is {x == "high"}" breaks the string. Use single quotes inside: f"It is {x == 'high'}".

Confusing .2f with "two digits". .2f is two decimals, not two figures in total. For the total width you use the number before the dot: {x:8.2f}.

Aligning numbers to the left. It comes out ragged and makes magnitudes hard to compare. Text to the left (<), numbers to the right (>).

Relying on \t to build tables. It works as long as every value is shorter than the tab stop; as soon as one goes over, the column shifts out of line. Use explicit widths.

Not assigning the result of input(). input("Name: ") without variable = in front asks for the data and discards it.

A prompt with no trailing space. input("Name:") makes what is typed stick to the message. Always leave a space.

Assuming that input() returns a number. It is the mistake that defines this lesson: it always returns a str. If you are going to calculate, you will have to convert.

Prompts that do not explain the options. If only three priorities are accepted, write them in the prompt. The cost of a clear prompt is zero and it saves half the wrong entries.

Tip: separate input, process and output. Look at the structure of this lesson's program: first all the input() calls, then all the calculations, and finally all the presentation. It is easier to read, to test and to modify than if they are all mixed together. This separation is a design principle that holds in programs of any size.

Tip: define the width as a constant. WIDTH = 46 instead of repeating 46 on fifteen lines. Changing the card's layout becomes a matter of one line.

Tip: test with awkward data. Run your program and type an extremely long title, or press Enter without writing anything. The card will go out of line or come out empty. Note down what you saw: that is what needs validating.

Exercises

Exercise 1: Predicting the output

Write down exactly what each block prints, paying attention to spaces and line breaks.

# a)
print("Marta", "Luis", "Nuria", sep=" | ")

# b)
print("Task", end=" -> ")
print("completed")

# c)
hours = 7.456
print(f"{hours:.2f}")
print(f"{hours:.0f}")

# d)
name = "Luis"
print(f"[{name:<10}]")
print(f"[{name:>10}]")
print(f"[{name:^10}]")

# e)
print("Line 1\n\tLine 2 tabbed")

# f)
days = 5
print("There are {days} days left")
print(f"There are {days} days left")

# g)
print(f"{'Total':<8}{1250:>10,}")

Exercise 2: Alba Studio team card

Write a program team_summary.py that displays this table using only f-strings (no \t at all), with this data already written in variables:

Person Pending tasks Hours spent Load percentage
Marta 3 12.5 0.25
Luis 5 27.0 0.45
Nuria 2 9.25 0.30

Requirements:

  • Header centred within a width of 44 characters, with lines of = above and below.
  • Names aligned to the left; numbers to the right.
  • Hours with one decimal; the load percentage shown as a percentage with one decimal (remember {x:.1%}).
  • The total width must be in a constant and used on every line.

Exercise 3: Interactive task registration

Write task_registration.py that:

  1. Displays a welcome header.
  2. Asks from the keyboard, with clear prompts: title, client, assignee and priority.
  3. Displays a card with the four items lined up in two columns.
  4. At the end, displays on a single line, using an f-string with an expression inside, whether the task is high priority.
  5. Prints the type() of the priority variable and explains in a comment what the result means.

Solutions

Solution 1.

a) Marta | Luis | Nuria — the sep replaces the space with " | ".

b)

Task -> completed

A single line: the first print does not break because end=" -> ".

c)

7.46
7

.2f rounds to two decimals (7.456 → 7.46) and .0f to zero decimals (7.456 → 7).

d)

[Luis      ]
[      Luis]
[   Luis   ]

Ten characters of width in all three cases; what changes is where the text is placed. When centring with an odd amount of padding left over, the extra gap goes on the right.

e)

Line 1
	Line 2 tabbed

\n breaks the line and \t inserts a tab at the start of the second one.

f)

There are {days} days left
There are 5 days left

Without the f, the braces are literal text. It is the most common slip.

g) Total 1,250Total takes up 8 characters on the left, and 1250 is displayed with a thousands separator within 10 characters on the right.

Solution 2.

# team_summary.py - Alba Studio

WIDTH = 44

marta_tasks = 3
marta_hours = 12.5
marta_load = 0.25

luis_tasks = 5
luis_hours = 27.0
luis_load = 0.45

nuria_tasks = 2
nuria_hours = 9.25
nuria_load = 0.30

print("=" * WIDTH)
print(f"{'TEAM SUMMARY - Alba Studio':^{WIDTH}}")
print("=" * WIDTH)
print(f"{'PERSON':<14}{'TASKS':>8}{'HOURS':>11}{'LOAD':>11}")
print("-" * WIDTH)
print(f"{'Marta':<14}{marta_tasks:>8}{marta_hours:>11.1f}{marta_load:>11.1%}")
print(f"{'Luis':<14}{luis_tasks:>8}{luis_hours:>11.1f}{luis_load:>11.1%}")
print(f"{'Nuria':<14}{nuria_tasks:>8}{nuria_hours:>11.1f}{nuria_load:>11.1%}")
print("=" * WIDTH)

Output:

============================================
         TEAM SUMMARY - Alba Studio
============================================
PERSON           TASKS      HOURS       LOAD
--------------------------------------------
Marta                3       12.5      25.0%
Luis                 5       27.0      45.0%
Nuria                2        9.2      30.0%
============================================

Notice the widths chosen: 14 + 8 + 11 + 11 = 44, exactly WIDTH. When the columns add up to the total width, the lines of = and - fit the table and the result looks deliberate, which is just what we are after.

Observe too that 9.25 with .1f is displayed as 9.2 and not as 9.3: faced with an exact tie, Python rounds to the nearest even digit (so-called banker's rounding). It is not an error, but it is worth knowing when you present figures to a client.

Solution 3.

# task_registration.py - Alba Studio

WIDTH = 44

print("=" * WIDTH)
print(f"{'NEW TASK REGISTRATION':^{WIDTH}}")
print(f"{'Alba Studio':^{WIDTH}}")
print("=" * WIDTH)
print()

title = input("Task title                   : ")
client = input("Client                       : ")
assignee = input("Assignee (Marta/Luis/Nuria)  : ")
priority = input("Priority (high/medium/low)   : ")

print()
print("-" * WIDTH)
print(f"{'CARD':^{WIDTH}}")
print("-" * WIDTH)
print(f"{'Title':<14}{title:>{WIDTH - 14}}")
print(f"{'Client':<14}{client:>{WIDTH - 14}}")
print(f"{'Assignee':<14}{assignee:>{WIDTH - 14}}")
print(f"{'Priority':<14}{priority:>{WIDTH - 14}}")
print("-" * WIDTH)

print(f"High priority: {priority == 'high'}")

# <class 'str'>: input() ALWAYS returns text, even when the user
# types a number. That is why the comparison above only works
# if they write exactly "high" in lower case.
print(type(priority))

Example run:

============================================
           NEW TASK REGISTRATION
                Alba Studio
============================================

Task title                   : Book fair poster
Client                       : Booksellers association
Assignee (Marta/Luis/Nuria)  : Nuria
Priority (high/medium/low)   : medium

--------------------------------------------
                    CARD
--------------------------------------------
Title                       Book fair poster
Client               Booksellers association
Assignee                               Nuria
Priority                              medium
--------------------------------------------
High priority: False
<class 'str'>

Try running it again writing HIGH for the priority: the card will come out just as correct, but High priority will still say False. That is the problem we solve next.

Conclusion

Your program now talks to the world. You know that print() accepts several arguments separated by commas — converting each one to text automatically — and that its sep and end parameters control what goes between the values and what goes at the end. You handle the escape characters \n, \t, \" and \\, and you know how to deal with quotes inside text in three different ways.

Above all, you have mastered f-strings: interpolating variables and expressions between braces, rounding decimals with .2f, and lining up columns with <, > and ^ to produce readable tables without depending on the tab. You also recognise .format() and the old % when you meet them in other people's code, even though you always write f-strings.

And you know input(): how it stops the program, how to give it a useful message and — the point that governs the next lesson — that it always returns a str, even when the user types a number.

EasyTask is at version 0.4 and already looks like a tool: Marta runs it, types the title, the description, the assignee and the priority of the job, and gets a lined-up card with the deadline and progress calculations. Nobody has to open the editor to register a new task.

But we have left two very visible loose ends. The first: the numeric data — estimated days, hours spent — is still written in the code, because what comes from input() is text and "3" + 1 gives a TypeError. The second: the program swallows anything the user types at it. If Marta writes HIGH, the comparison fails; if she writes superurgent, it is stored as it is; if she presses Enter without writing, the task is left with no title.

Both are the same problem seen in two ways: data coming from outside cannot be trusted. In Type Conversion and Validation you will learn to convert it to the right type with int(), float() and str(), to clean it up with .strip() and .lower() — which will at last make "HIGH" worth as much as "high", the loose end we left in lesson 01-05 — and to check that it is valid before using it. With that we will close module 2 and EasyTask will be, for the first time, a robust program.

© Copyright 2026. All rights reserved