This lesson closes module 2 by tying up two loose ends we have been dragging along for some time.

The first was discovered with pencil and paper, in the desk check of the lesson From Problem to Algorithm: when Marta typed HIGH in upper case, the algorithm rejected a perfectly correct priority, because to a computer "HIGH" and "high" are two different pieces of text. We promised to solve it here.

The second appeared in the previous lesson: input() always returns a str, so as soon as EasyTask wants to ask for the estimated days or the hours spent and do sums with them, it will crash with a TypeError. That is why that data is still written by hand in the code.

Both are manifestations of the same principle, which is the underlying idea of this lesson: any data coming from outside the program is suspect until proven otherwise. It may have the wrong type, have spare spaces, come in upper case, be empty or be plain nonsense. The job of converting it to the right type is called conversion; that of checking it makes sense, validation. A program that does not do both is not a finished program: it is a prototype that works as long as nobody uses it in earnest.

You will learn to convert with int(), float(), str() and bool(), to interpret the ValueError and TypeError errors, to clean up text with .strip(), .lower() and .capitalize(), and to check data before using it with .isdigit(), .isalpha() and the in operator.

Contents

  1. The two loose ends, in code
  2. Explicit conversion: int(), float(), str()
  3. Which conversions work and which fail: the ValueError
  4. Implicit conversion, and why text and numbers do not mix
  5. bool() and the truthiness rules
  6. Normalising the text: .strip(), .lower() and .capitalize()
  7. Checking before converting: .isdigit() and .isalpha()
  8. Validating against a list of allowed values
  9. What validating is and why you always validate
  10. A minimal if, and what we are still missing
  11. EasyTask: cleaning up and validating the input
  12. Common mistakes and tips
  13. Exercises
  14. Conclusion

  1. The two loose ends, in code

Let's start by seeing the two problems in all their rawness.

Loose end 1: what comes from input() is text.

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

print(type(days))        # <class 'str'>
print(days + 1)          # TypeError
TypeError: can only concatenate str (not "int") to str

Loose end 2: text is not compared the way a person writes it.

priority = input("Priority: ")       # the user types HIGH

print(priority == "high")            # False

And there are more variants of the same problem, all of them plausible:

What the user types priority == "high" Why
high True It matches exactly
HIGH False Different case
High False The first letter differs
high (trailing space) False The space is one more character
high (leading space) False The same
urgent False It is not a valid priority
(Enter without typing anything) False It is the empty string ""

Of the seven rows, only the first is what we wanted, the last is genuinely a user mistake, and the five in between are reasonable entries that a decent program should accept or reject on purpose, not by accident.

The first five are fixed by converting and normalising; the last two, by validating. Let's get to it.

  1. Explicit conversion: int(), float(), str()

Python comes with functions that transform a value from one type to another. They are named after the target type and are used by putting the value in parentheses.

Function Converts to Example Result
int(x) Integer int("3") 3
float(x) Decimal float("3.5") 3.5
str(x) Text str(3.5) "3.5"
bool(x) Boolean bool("") False

This is called explicit conversion or casting: you are the one asking for it, at a specific point in the code.

int(): from text to integer

It solves the first loose end at a stroke:

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

days = int(days_text)

print(type(days))            # <class 'int'>
print(days + 1)              # 4    <- now it really is arithmetic
print(days * 2)              # 6    <- it no longer repeats the text

It is common to write it all on one line, wrapping the input() directly:

days = int(input("Estimated days: "))

It reads from the inside out: first input() runs and returns text; that text is passed to int(), which returns an integer; and the integer is assigned to days. It is compact and very common, but it has a drawback we will see in section 7: if the user types something that is not a number, the program stops right there.

int() also converts decimals to integers, and here a detail deserves attention:

print(int(3.9))      # 3    <- it does NOT round to 4
print(int(3.1))      # 3
print(int(-3.9))     # -3   <- it cuts towards zero

int() truncates, it does not round: it removes the decimal part without looking at it. If you want to round to the nearest integer, use round():

print(round(3.9))    # 4
print(round(3.1))    # 3

Confusing int() with round() produces silent errors in calculations of amounts and percentages. Note it down.

float(): to decimal

hours = float(input("Hours spent: "))         # the user types 7.5

print(hours)         # 7.5
print(type(hours))   # <class 'float'>

float() also accepts text with no decimals, and converts it by adding .0:

print(float("3"))    # 3.0

But it does not accept the continental decimal comma:

print(float("3,5"))
ValueError: could not convert string to float: '3,5'

It is a real problem with users in countries where the comma is the decimal separator, who type it perfectly naturally. A careful program replaces the comma with a dot before converting. That is done with the .replace() method, which belongs to Strings; I mention it so that you know a solution exists.

str(): to text

It is the reverse conversion: it turns any value into its textual representation.

days = 7
completed = False
date = None

print(str(days))          # 7      (but now it is a str)
print(str(completed))     # False
print(str(date))          # None

print(type(str(days)))    # <class 'str'>

Its classic use is concatenating a number with text using +, which without conversion gave a TypeError:

days = 7

print("There are " + days + " days left")          # TypeError
print("There are " + str(days) + " days left")     # There are 7 days left

That said: in new code you will hardly ever need str() for this, because the f-strings of the previous lesson do the conversion by themselves and look far better:

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

str() is still useful in other contexts — we used it in the EasyTask card to line up a boolean as if it were text — but not as the usual way of composing messages.

  1. Which conversions work and which fail: the ValueError

Converting is not always possible. This table sums up what you can expect:

Conversion Result Does it work?
int("3") 3 Yes
int(" 3 ") 3 Yes — int() ignores spaces at the ends
int("3.0") NoValueError
int("three") NoValueError
int("") NoValueError
int(3.9) 3 Yes (it truncates)
int(True) 1 Yes (True is 1, False is 0)
float("3.5") 3.5 Yes
float("3") 3.0 Yes
float("3,5") NoValueError
float("abc") NoValueError
str(anything) Its text It always works
bool(anything) True or False It always works

Three rows deserve a comment:

  • int(" 3 ") works. int() is tolerant of spaces at the ends. It is not tolerant of anything else.
  • int("3.0") fails. It is surprising, because 3.0 is a number. But for int(), the text "3.0" contains a dot that does not belong to an integer. If you expect the user to be able to write decimals, convert with float() and truncate afterwards: int(float("3.0")) gives 3.
  • str() and bool() never fail. Any value has a textual representation and any value is true or false. That makes them safe, but also silent: they will not warn you about an absurd piece of data.

Reading a ValueError

When the conversion cannot be done, Python raises a ValueError: the type was right (you passed int() a piece of text, which is what it expects), but the specific value is no good.

days = int("three")
Traceback (most recent call last):
  File "easytask.py", line 4, in <module>
    days = int("three")
           ^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'three'

A Python error message is read from the bottom up, and it contains more information than it seems:

Part What it tells you
ValueError The type of error: the value cannot be converted
invalid literal for int() You were trying to convert to an integer
with base 10 In base ten (the normal one)
'three' The exact value that failed
File "easytask.py", line 4 The file and the line where it happened

That last row is the most valuable and the one most people ignore: the error tells you where the problem is. Do not read only the last line; look at the line number too.

And do not confuse the two errors that show up when converting:

Error It means Example
ValueError The value is no good for that conversion int("three")
TypeError The type does not support that operation "3" + 4

When a ValueError happens, the program stops. Whatever came after it does not run. There are two ways of avoiding it: checking before converting (which we will do in section 7) or catching the error when it happens, with try / except, which is studied in Debugging and Error Handling.

  1. Implicit conversion, and why text and numbers do not mix

There are conversions Python does by itself, without being asked. They are called implicit, and they happen only between numeric types.

result = 3 + 1.5

print(result)            # 4.5
print(type(result))      # <class 'float'>

Python has converted the 3 (an int) to 3.0 (a float) so as to be able to add it to 1.5. The rule is simple: when int and float are mixed, the result is always float, because it is the "wider" type, the one that can represent the other's values without losing information.

print(type(5 + 2))       # <class 'int'>
print(type(5 + 2.0))     # <class 'float'>
print(type(5 / 2))       # <class 'float'>   <- remember: / always gives float
print(type(5 // 2))      # <class 'int'>

Booleans take part too, because internally True is worth 1 and False is worth 0:

print(True + True)       # 2
print(False + 10)        # 10

It is a curiosity more than a technique; do not write code that depends on it.

Why Python does not convert text and numbers

And now the question everybody asks: if Python converts int to float without being asked, why does it not convert "3" to 3?

print("3" + 4)
TypeError: can only concatenate str (not "int") to str

Because it would be ambiguous. "3" + 4 has two equally reasonable interpretations:

  • As addition: 3 + 47.
  • As concatenation: "3" + "4""34".

Python does not guess. It is a deliberate design decision, and a very sound one: it prefers to stop and warn you rather than do something different from what you wanted. Other languages do guess, and that convenience costs them dear. In JavaScript, "3" + 4 gives "34" and "3" - 4 gives -1: the same pair of values behaves as text with one operator and as a number with another, which is an inexhaustible source of hard-to-find bugs.

The practical moral: when you see a TypeError mentioning str and int, you have a piece of data with the wrong type. Almost always it is an unconverted input().

flowchart TD
    A["I mix two values<br/>in one operation"] --> B{"Are both<br/>numeric?"}
    B -->|"int and float"| C["Python converts by itself<br/>Result: float"]
    B -->|"str and number"| D["TypeError<br/>Convert yourself with int(), float() or str()"]

  1. bool() and the truthiness rules

bool() converts any value to True or False, and it never fails. But you have to know its rules, because they are not obvious.

In Python, a handful of values are considered false; all the rest are true.

Value bool(value) Comment
0 False Integer zero
0.0 False Decimal zero
"" False Empty string
None False Absence of value
False False Obviously
" " True A space is not empty: it has one character
"0" True It is a one-character piece of text, not the number zero
"False" True It is text! And it is not empty
-1 True Any number other than zero
3.14 True The same

The three dangerous rows are " ", "0" and "False". All three look as though they should give False and all three give True, because they are non-empty text. bool() on a piece of text asks only one thing: does it have at least one character?

print(bool(""))          # False
print(bool(" "))         # True    <- careful
print(bool("False"))     # True    <- careful
print(bool("0"))         # True    <- careful

This has a direct and extremely important application for us: detecting whether the user pressed Enter without typing anything.

title = input("Title: ")             # the user presses Enter straight away

has_title = bool(title)
print(has_title)                     # False

Since input() returns "" when nothing is typed, and "" is false, bool(title) tells us whether there is something there. In practice it is written more briefly: the text's own value already serves as a condition, without wrapping it in bool(). It is what Python calls an object's truth value.

But watch out for the trap that follows from the table: if the user types only spaces, title is worth " ", which is true. A title of three spaces would get past the filter. The solution is to remove the spaces before checking, and that is what the next section is about.

One last warning: never use bool() to interpret an answer from the user.

answer = input("Completed? (True/False): ")        # the user types False

print(bool(answer))        # True    <- NOT what you expected

"False" is non-empty text, so bool() gives True. To interpret a yes/no you have to compare the text, not convert it:

completed = answer.strip().lower() == "yes"

  1. Normalising the text: .strip(), .lower() and .capitalize()

Normalising is transforming a piece of data into a canonical form before comparing or storing it, so that irrelevant differences — spare spaces, capitals — stop mattering.

Python offers many string methods; you will see them all in Strings. Here we stick to the three that are essential for cleaning up input.

They are called with a dot after the text variable: variable.method().

.strip(): removing spaces from the ends

entry = "  high  "

print(entry)                # '  high  ' (with spaces)
print(entry.strip())        # 'high'

It removes spaces, tabs and line breaks at the beginning and at the end, but not the ones inside:

print("  Sole  Bakery  ".strip())        # 'Sole  Bakery'

It is the first operation any text coming from a keyboard should undergo. Spare spaces are invisible on screen and break every comparison.

.lower(): putting everything in lower case

Here it is, at last, the solution to the loose end from lesson 01-05:

priority = "HIGH"

print(priority == "high")              # False
print(priority.lower() == "high")      # True

There is also .upper(), which does the opposite. The technique is called case-insensitive comparison, and it consists of bringing both sides to the same case before comparing. Since the literal "high" is already in lower case, it is enough to lower the other one.

The normal thing is to chain the two operations. Methods are applied from left to right:

priority = input("Priority: ")           # the user types "  HIGH  "

priority = priority.strip().lower()

print(priority)                          # 'high'
print(priority == "high")                # True

Read it like this: the spaces are removed from the original text, and the result is lowered. And notice something important about the second line: the result is reassigned to the variable itself. If you wrote only priority.strip().lower() without the priority = in front, the result would be calculated and thrown away, because string methods do not modify the original text: they return a new one. It is the immutability of strings you saw in lesson 02-01, now with practical consequences.

priority = "  HIGH  "
priority.strip().lower()       # it is calculated and lost
print(priority)                # '  HIGH  '  <- nothing has changed

This is one of the most frequent mistakes among beginners. Remember it: string methods return, they do not modify.

.capitalize(): first letter in upper case

Useful for proper names, which is exactly the case of a task's assignee:

print("nuria".capitalize())      # Nuria
print("NURIA".capitalize())      # Nuria
print("nUrIa".capitalize())      # Nuria

It puts the first letter in upper case and the rest in lower case. That last bit gets forgotten and sometimes comes as a surprise:

print("ana maria".capitalize())  # Ana maria   <- only the first word

To capitalise the initial of every word there is .title(), with limitations of its own. Since the Alba Studio team are three single-word names, .capitalize() is enough for us.

Method What it does " nUrIa "
.strip() Removes spaces from the ends "nUrIa"
.lower() Everything to lower case " nuria "
.upper() Everything to upper case " NURIA "
.capitalize() First letter upper case, rest lower case " nuria "
.strip().capitalize() Chained "Nuria"

Notice the second-to-last row: .capitalize() on a piece of text starting with a space does nothing visible, because the "first letter" is a space. The .strip() always goes first.

  1. Checking before converting: .isdigit() and .isalpha()

We already know that int("three") stops the program. The way to avoid it without resorting to error handling yet is to ask first whether the conversion is possible.

.isdigit() answers this: is this text made up only of digits? It returns a boolean.

print("3".isdigit())         # True
print("300".isdigit())       # True
print("three".isdigit())     # False
print("3.5".isdigit())       # False   <- the dot is not a digit
print("-5".isdigit())        # False   <- nor is the sign
print("".isdigit())          # False   <- empty string
print(" 3".isdigit())        # False   <- nor is the space

It is a strict check, and that strictness is a virtue: if .isdigit() returns True, int() will work for certain. But bear its three limits in mind: it does not accept decimals, it does not accept negatives and it does not tolerate spaces. That is why the .strip() must come first:

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

entry = entry.strip()
print(entry.isdigit())                   # True

.isalpha() is its equivalent for letters: is it made up only of letters?

print("Luis".isalpha())          # True
print("Nuria3".isalpha())        # False
print("Luis Gomez".isalpha())    # False   <- the space is not a letter
print("".isalpha())              # False

It serves to detect that somebody has written a number where a name belonged. Careful with the third line: a compound name contains a space and does not pass the filter, so .isalpha() is suitable for single-word names, like those of the Alba Studio team, but not for first names and surnames.

Method Returns True when... Cases it rejects
.isdigit() There are only digits and there is at least one "", "3.5", "-5", "3 "
.isalpha() There are only letters and there is at least one "", "Luis1", "Luis Gomez"
.isspace() There is only whitespace "", any text with content

The safe usage pattern comes out like this:

1. Read with input()
2. Clean with .strip()  (and .lower() if it is an option from a set)
3. Check with .isdigit() / in (...) / length
4. Only if the check passes: convert with int() or float()

That order is not negotiable: clean, check, convert.

  1. Validating against a list of allowed values

When a piece of data can take only a few values — like the EasyTask priority, which according to requirement R2 accepts only high, medium and low — the check is written with the in operator you learned in lesson 02-02:

priority = input("Priority (high/medium/low): ")
priority = priority.strip().lower()

valid_priority = priority in ("high", "medium", "low")
print(valid_priority)

With that prior normalisation, all these entries are correctly accepted: high, HIGH, High, high , hIgH. And these are rejected, as they should be: urgent, highest, `` (empty), 1.

The same goes for the assignee, who according to R3 must be one of the three people at the studio:

assignee = input("Assignee (Marta/Luis/Nuria): ")
assignee = assignee.strip().capitalize()

valid_assignee = assignee in ("Marta", "Luis", "Nuria")
print(valid_assignee)

Notice the difference in normalisation: for the priority we use .lower(), because the canonical values are in lower case; for the assignee we use .capitalize(), because they are proper names and we want to store them properly written. Normalisation must bring the data to the form in which you are going to store it, not to just any form.

Advantages of this form over chaining comparisons with or:

  • It reads like the business rule: "the priority must be one of these three".
  • Adding an allowed value means adding an element, not another comparison.
  • You cannot fall into the priority == "high" or "medium" trap we saw in 02-02.

  1. What validating is and why you always validate

Let's recap calmly, because this section is the conceptual heart of the lesson.

Validating is checking that a piece of data meets the conditions needed for it to be used, before using it. It is not a type check: it is a sense check.

A piece of data can fail at four different levels, and it is worth distinguishing them:

Level Question Example of failure Tool
Presence Is there anything? Empty title .strip() and check whether anything is left
Type Can it be converted? Days = "three" .isdigit(), then int()
Domain Is it among the allowed values? Priority = "urgent" in ("high", "medium", "low")
Coherence Does it make sense? Estimated days = -5 or 9000 Comparisons: 1 <= days <= 365

That fourth level is always forgotten and it is where the most absurd data slips through. "-5" would not pass .isdigit(), but "0" would, and a task of zero estimated days means nothing. Converting properly is not enough: you have to ask yourself whether the value makes sense in the business.

And why is everything that comes from outside validated, always, without exception? For three reasons of increasing weight:

  1. Robustness. A program that stops with a ValueError because somebody typed one letter too many is a program nobody will want to use. Marta should not have to phone anyone because the program "crashed".

  2. Data integrity. If EasyTask accepts the priority "superurgent", that task will never appear when filtering by high, medium or low priority. The program will not fail: it will lie, which is far worse. A visible error gets fixed; a corrupted piece of data spreads for months.

  3. Security. In programs connected to databases or to the internet, unvalidated data is the way in for the most common attacks in the world. It is beyond the scope of this course, but the habit you are building now is exactly the one that prevents those problems.

And the golden rule, which is worth learning word for word:

Never trust data coming from outside the program. Not from the keyboard, not from a file, not from the network. Not even if the person typing it is you.

That last clause is not rhetoric. A programmer is the worst tester of their own program, because they always type what the program expects. Marta does not.

  1. A minimal if, and what we are still missing

So far we have calculated booleans (valid_priority, has_title) and printed them. But validating is of little use if the program does not do something different depending on the result.

That is what if is for, and it is studied in depth in the next lesson, Conditionals. Here we use it in its simplest form, just so that the validation has a visible effect:

priority = input("Priority (high/medium/low): ")
priority = priority.strip().lower()

if priority in ("high", "medium", "low"):
    print(f"Priority accepted: {priority}")
else:
    print("Invalid priority. 'medium' is assigned by default.")
    priority = "medium"

print(f"Final priority: {priority}")

Three notes on the syntax, without going on because it is not this lesson's subject:

  • The if line ends with a colon (:).
  • What goes below is indented (4 spaces), and that indentation is what tells Python which statements belong to each branch. In Python indentation is not cosmetic: it is syntax.
  • else marks the alternative path, the one taken when the condition is false.

With this, validation no longer only informs: it acts. It rejects the incorrect value and puts a default one in.

Even so, two limitations remain that this module cannot solve:

We cannot ask again. The natural thing would be to insist until the user writes something valid, exactly as we designed in the pseudocode of lesson 01-05 (that WHILE valid_priority = FALSE). Repeating requires a loop, and loops arrive in Loops; the full application to an EasyTask menu, in Interactive Menus. For now, the strategy is to assign a default value and warn.

We cannot catch the error of a conversion. If the user types "three" where a number belongs, our defence is to check first with .isdigit(). The other route — letting the conversion fail and trapping the ValueError with try / except — is more powerful and more common in professional code, and it is studied in Debugging and Error Handling.

Strategy How it works Where it is studied
Check first (.isdigit()) The error is avoided by asking first Here
Default value (if / else) A reasonable alternative is accepted Here, in depth in 03-01
Ask again You insist until you get a valid value 03-02 and 03-04
Catch the error (try / except) You let it fail and manage the fall 08-02

All four are valid and they combine. You will end the course using all four.

  1. EasyTask: cleaning up and validating the input

Version 0.5 of easytask.py. We pick up 0.4 from the previous lesson and add what we have learned: we normalise the text, we convert the numbers we can now ask for from the keyboard, and we validate the four levels of section 9.

# easytask.py - Alba Studio
# Version 0.5: input cleaned up, converted and validated

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

PRIORITIES = ("high", "medium", "low")
TEAM = ("Marta", "Luis", "Nuria")
DEFAULT_DAYS = 5

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

# ============================================================
# INPUT + CLEAN-UP + VALIDATION
# ============================================================

# --- 1. Title: presence ---
title = input("Task title                  : ")
title = title.strip()

title_warning = ""
if title == "":
    title = "(no title)"
    title_warning = "WARNING: the title was empty."

# --- 2. Assignee: domain ---
assignee = input("Assignee (Marta/Luis/Nuria) : ")
assignee = assignee.strip().capitalize()

assignee_warning = ""
if assignee not in TEAM:
    assignee_warning = f"WARNING: '{assignee}' is not on the team. Assigned to Marta."
    assignee = "Marta"

# --- 3. Priority: domain ---
priority = input("Priority (high/medium/low)  : ")
priority = priority.strip().lower()

priority_warning = ""
if priority not in PRIORITIES:
    priority_warning = f"WARNING: '{priority}' is not a priority. Assigned 'medium'."
    priority = "medium"

# --- 4. Estimated days: type + coherence ---
days_text = input("Estimated days              : ")
days_text = days_text.strip()

days_warning = ""
if days_text.isdigit():
    estimated_days = int(days_text)
    if estimated_days < 1 or estimated_days > 365:
        days_warning = f"WARNING: {estimated_days} days is not reasonable. Assigned {DEFAULT_DAYS}."
        estimated_days = DEFAULT_DAYS
else:
    days_warning = f"WARNING: '{days_text}' is not a number. Assigned {DEFAULT_DAYS} days."
    estimated_days = DEFAULT_DAYS

# ============================================================
# PROCESS
# ============================================================

estimated_hours = estimated_days * HOURS_PER_WORKDAY
completed = False
is_urgent = priority == "high" and not completed

# ============================================================
# OUTPUT
# ============================================================

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"{'Estimated days':<16}{estimated_days:>{WIDTH - 16}}")
print(f"{'Estimated hours':<16}{estimated_hours:>{WIDTH - 16}}")
print(f"{'Urgent':<16}{str(is_urgent):>{WIDTH - 16}}")
print("=" * WIDTH)

# --- Validation warnings ---
print()
print(title_warning)
print(assignee_warning)
print(priority_warning)
print(days_warning)

A run with "dirty" but reasonable entries, of the kind Marta would type on a busy Monday:

==============================================
                EasyTask v0.5
       Task registration - Alba Studio
==============================================

Task title                  :   Sole brand manual
Assignee (Marta/Luis/Nuria) : LUIS
Priority (high/medium/low)  :   HIGH
Estimated days              :  4

==============================================
                  TASK CARD
==============================================
Title                        Sole brand manual
Assignee                                  Luis
Priority                                  high
Estimated days                               4
Estimated hours                             32
Urgent                                    True
==============================================

The four entries came in with spaces or in upper case, and all four have been accepted and stored in their canonical form. In version 0.4, that same run would have produced Urgent: False, because " HIGH " was not "high". The loose end from lesson 01-05 is tied up.

And now a run with genuinely wrong entries:

Task title                  :
Assignee (Marta/Luis/Nuria) : Pedro
Priority (high/medium/low)  : superurgent
Estimated days              : four

==============================================
                  TASK CARD
==============================================
Title                               (no title)
Assignee                                 Marta
Priority                                medium
Estimated days                               5
Estimated hours                             40
Urgent                                   False
==============================================

WARNING: the title was empty.
WARNING: 'Pedro' is not on the team. Assigned to Marta.
WARNING: 'superurgent' is not a priority. Assigned 'medium'.
WARNING: 'four' is not a number. Assigned 5 days.

The program has not stopped at any point. Before, int("four") would have raised a ValueError and execution would have died right there, with no card and no explanation. Now every incorrect item is detected, replaced with something reasonable and reported.

Let's go over the design decisions, because they are as important as the code:

  • PRIORITIES and TEAM are constants in upper case. The lists of allowed values should not be written inside the check: placed at the top, they can be seen at a glance and changed in one place.
  • The order is always the same: read → .strip() → normalise → check → convert. Never convert before checking.
  • Each item uses the normalisation that suits it: .lower() for the priority (a catalogue value in lower case), .capitalize() for the assignee (a proper name), only .strip() for the title (it is free text; lowering it would spoil it).
  • The warnings are stored in variables and printed at the end, so that the card comes out clean and the problems are grouped together. When there is no error, the variable is worth "" and an empty line is printed.
  • The days are validated on two levels: first that they are a number (.isdigit()), then that the number makes sense (1 <= days <= 365). A 0 would have passed the first filter and not the second.

What is still improvable

Be honest about this version, because module 3 starts right here:

Limitation Solved in
Faced with an incorrect item it sets a default value instead of asking again Loops
The four validation blocks repeat the same structure Defining and Using Functions
Only one task can be registered per run Interactive Menus and Lists
When the program closes, the task is lost Saving Data to Files
Empty warnings print blank lines Conditionals

Common Mistakes and Tips

Forgetting to convert what input() returns. It is the number one mistake of the module. If you are going to calculate, convert. If the TypeError mentions str and int, look at your input() calls.

Converting before checking. int(input(...)) is convenient and fragile: any letter brings the program down. The safe order is read, clean, check and only then convert.

Believing that int() rounds. int(3.9) is 3. To round, round().

Expecting int("3.0") to work. It does not: the dot does not belong to an integer. Use int(float("3.0")) if you need to accept that form.

Forgetting that string methods do not modify. title.strip() without assigning does nothing. It has to be title = title.strip().

Normalising only one side of the comparison. priority.lower() == "High" is still False always. If you lower one side, the other must already be in lower case.

Applying .capitalize() before .strip(). " nuria".capitalize() changes nothing visible, because the first "letter" is a space. The .strip() goes first.

Relying on bool() to interpret answers. bool("False") is True, and so is bool("0"). For yes/no, compare the normalised text.

Checking for emptiness without cleaning first. title == "" does not detect a title of three spaces. title.strip() == "" does.

Using .isdigit() with negatives or decimals. "-5" and "3.5" return False. If your data accepts decimals or a sign, .isdigit() is not the right check.

Validating only the type and forgetting coherence. "0" and "99999" are perfectly valid digits, and neither of the two is a reasonable number of days. Always ask yourself whether the value makes sense in the problem, not just whether it is convertible.

Tip: normalise at the moment of reading. Leave value = input(...).strip() right at the point of reading, and from then on always work with the clean data. If the cleaning is scattered around the program, sooner or later there will be a path that skips it.

Tip: put the valid options in constants. PRIORITIES = ("high", "medium", "low") at the top of the file. The business rule stays visible and gets changed in a single place.

Tip: test your program as if you wanted to break it. Press Enter without typing. Type spaces. Write in upper case. Put a letter where a number belongs. Put in a negative number. Everything that breaks the program is a missing validation.

Exercises

Exercise 1: Predicting the result

State the result — or the error, by name — of each expression, without running anything.

a)  int("42")
b)  int("42.0")
c)  int(42.9)
d)  round(42.9)
e)  float("7")
f)  float("7,5")
g)  str(True) + " value"
h)  bool("")
i)  bool(" ")
j)  bool("0")
k)  "  High  ".strip().lower()
l)  "  high  ".lower().strip() == "high"
m)  "LUIS".capitalize()
n)  "  luis".capitalize()
o)  "12".isdigit()
p)  "-12".isdigit()
q)  "3 days".isdigit()
r)  "Nuria" in ("Marta", "Luis", "Nuria")
s)  "nuria" in ("Marta", "Luis", "Nuria")
t)  "3" + 4

Exercise 2: Cleaning up an hours entry

Write a program log_hours.py that asks from the keyboard for the name of a person on the team and the hours spent on a task (a whole number), and that:

  1. Cleans both entries of spare spaces.
  2. Normalises the name to the form Name (initial capital, rest lower case).
  3. Checks that the name is on the team (Marta, Luis, Nuria); if not, warns and assigns "Marta".
  4. Checks that the hours are a whole number between 1 and 40; if not, warns and assigns 8.
  5. Calculates and displays the full 8-hour working days and the spare hours (remember // and % from lesson 02-02).
  6. Displays a card lined up with f-strings.

Test your program with these four entries and check that none of them stops it: NURIA / 12, then pedro / 8, then Luis / eight, and finally Luis / 100.

Exercise 3: Validating the four levels

For each of these EasyTask items, state at which validation level it fails (presence, type, domain or coherence), which tool from this lesson detects it and what the program should do. Then write the fragment of code that checks it.

Item Value entered
a Title: " "
b Priority: "URGENT"
c Estimated days: "fifteen"
d Estimated days: "0"
e Assignee: "nuria"
f Hours spent: "50" against 8 estimated hours

Solutions

Solution 1.

Expression Result
a int("42") 42
b int("42.0") ValueError: the dot does not belong to an integer
c int(42.9) 42 (it truncates, it does not round)
d round(42.9) 43
e float("7") 7.0
f float("7,5") ValueError: the comma is not a decimal separator
g str(True) + " value" "True value"
h bool("") False
i bool(" ") True — a space is a character
j bool("0") True — it is non-empty text
k " High ".strip().lower() "high"
l " high ".lower().strip() == "high" True — here the order does not matter, but .strip() first is the correct habit
m "LUIS".capitalize() "Luis"
n " luis".capitalize() " luis" — the first "letter" is a space, nothing changes
o "12".isdigit() True
p "-12".isdigit() False — the sign is not a digit
q "3 days".isdigit() False
r "Nuria" in ("Marta", "Luis", "Nuria") True
s "nuria" in ("Marta", "Luis", "Nuria") False — it is case sensitive; .capitalize() is missing
t "3" + 4 TypeError

Rows n and s are the ones most often got wrong, and both point to the same thing: normalising badly is as dangerous as not normalising, because it gives a false sense of security.

Solution 2.

# log_hours.py - Alba Studio

WIDTH = 40
TEAM = ("Marta", "Luis", "Nuria")
HOURS_PER_WORKDAY = 8
DEFAULT_HOURS = 8

print("=" * WIDTH)
print(f"{'HOURS LOG':^{WIDTH}}")
print("=" * WIDTH)
print()

# --- Name: clean-up and domain validation ---
name = input("Person (Marta/Luis/Nuria): ")
name = name.strip().capitalize()

name_warning = ""
if name not in TEAM:
    name_warning = f"WARNING: '{name}' is not on the team. Assigned to Marta."
    name = "Marta"

# --- Hours: type and coherence validation ---
hours_text = input("Hours spent (1-40)       : ")
hours_text = hours_text.strip()

hours_warning = ""
if hours_text.isdigit():
    hours = int(hours_text)
    if hours < 1 or hours > 40:
        hours_warning = f"WARNING: {hours} h is outside the 1-40 range. Assigned {DEFAULT_HOURS}."
        hours = DEFAULT_HOURS
else:
    hours_warning = f"WARNING: '{hours_text}' is not a number. Assigned {DEFAULT_HOURS} h."
    hours = DEFAULT_HOURS

# --- Process ---
workdays = hours // HOURS_PER_WORKDAY
spare_hours = hours % HOURS_PER_WORKDAY

# --- Output ---
print()
print("-" * WIDTH)
print(f"{'Person':<18}{name:>{WIDTH - 18}}")
print(f"{'Hours spent':<18}{hours:>{WIDTH - 18}}")
print(f"{'Full workdays':<18}{workdays:>{WIDTH - 18}}")
print(f"{'Spare hours':<18}{spare_hours:>{WIDTH - 18}}")
print("-" * WIDTH)
print(name_warning)
print(hours_warning)

Behaviour with the four entries requested:

Entry Final name Final hours Workdays Spare Warning
NURIA / 12 Nuria 12 1 4 None
pedro / 8 Marta 8 1 0 Pedro is not on the team
Luis / eight Luis 8 1 0 'eight' is not a number
Luis / 100 Luis 8 1 0 100 h outside the range

None of the four stops the program. Notice the first row: " NURIA " becomes "Nuria" thanks to .strip().capitalize(), and that is why it passes the domain validation. Without that clean-up, Nuria — a real person on the team — would have been rejected.

Solution 3.

Item Level Tool What to do
a Title " " Presence title.strip() == "" Reject and ask again, or set "(no title)"
b Priority "URGENT" Domain .strip().lower() and then in PRIORITIES Reject and assign "medium" by default
c Days "fifteen" Type .isdigit() Do not convert; warn and use the default value
d Days "0" Coherence Comparison days >= 1 Convertible and numeric, but meaningless: reject
e Assignee "nuria" None after normalising .strip().capitalize() Accept: it is valid, it was just badly written
f Hours "50" against 8 estimated Coherence Comparison between two items Warn of a serious deviation from the budget

Checking code:

PRIORITIES = ("high", "medium", "low")
TEAM = ("Marta", "Luis", "Nuria")

# a) Presence
title = "   ".strip()
title_ok = title != ""
print(title_ok)           # False

# b) Domain
priority = "URGENT".strip().lower()
priority_ok = priority in PRIORITIES
print(priority_ok)        # False

# c) Type
days_text = "fifteen".strip()
days_type_ok = days_text.isdigit()
print(days_type_ok)       # False

# d) Coherence
days_text = "0".strip()
days_type_ok = days_text.isdigit()
print(days_type_ok)       # True   <- it passes the type filter
days = int(days_text)
days_range_ok = 1 <= days <= 365
print(days_range_ok)      # False  <- but not the coherence one

# e) Domain, after normalising
assignee = "nuria".strip().capitalize()
assignee_ok = assignee in TEAM
print(assignee)           # Nuria
print(assignee_ok)        # True

# f) Coherence between two items
hours_spent = int("50".strip())
estimated_hours = 8
serious_deviation = hours_spent > estimated_hours * 2
print(serious_deviation)  # True

Cases d and e are the ones that really teach. d shows that passing the type validation is not enough: "0" is a perfectly convertible digit and a task of zero days means nothing. And e shows the opposite: a piece of data that looked invalid ("nuria" is not in TEAM) turns out to be perfectly correct as soon as it is normalised. Validating without normalising first rejects good data; normalising without validating afterwards accepts bad data. Both are needed, and in that order.

Conclusion

This lesson closes module 2, and it also closes the two loose ends that opened it.

You know how to convert with int(), float() and str(), and you know their limits: int() truncates instead of rounding, int("3.0") does not work, nor does float("3,5"). You know how to read a ValueError — which tells you the exact value and the line where it failed — and to tell it apart from the TypeError, which appears when the type does not support the operation. You understand why Python converts int to float implicitly but refuses to mix text and numbers, and why that refusal is a virtue of the language and not a whim.

You know the truthiness rules of bool() and its three traps (" ", "0" and "False" are true). You normalise text with .strip(), .lower() and .capitalize(), remembering that string methods return a new piece of text and do not modify the original. You check before converting with .isdigit() and .isalpha(), and you validate against a catalogue of allowed values with in. And you have a mental framework so as not to forget anything: presence, type, domain and coherence, in that order.

Above all, you have internalised the principle that underpins all of the above: nothing that comes from outside the program is trustworthy. Not from the keyboard, not from a file, not from whoever types it, even if that is you.

EasyTask is at version 0.5 and has stopped being fragile. Marta can type HIGH , LUIS or Sole brand manual and the program understands it; she can write superurgent, Pedro or four and the program does not stop: it warns, corrects and carries on. The comparison that failed in the desk check of lesson 01-05 — that "HIGH" which was not "high" — works today.

Look back over what has been achieved in this module: a task's data lives in variables with the right type (02-01), it is combined in expressions that calculate deadlines, progress and urgencies (02-02), it comes in from the keyboard and goes out in a card lined up with f-strings (02-03), and it arrives cleaned up and validated (02-04). That is a program that gets used, not a program that gets read.

And yet you have noticed it in every section of this lesson: the program takes decisions by halves. It sets default values because it does not know how to ask again. It registers a single task and finishes. The if statements we have used were minimal and borrowed from the next lesson. All of that is control structures: the statements that let a program choose paths and repeat actions, and that turn a linear sequence into something that reacts.

That is exactly what starts in module 3, with Conditionals, where that if we have used on tiptoe is explained in full — if, elif, else, nested conditions, indentation — and EasyTask will finally move from warning about problems to deciding what to do with them.

© Copyright 2026. All rights reserved