None and Type Conversions
In Lesson 3, you wrote comparisons that produce booleans and combined them with and, or, and not. Now you meet a value that is none of the four types you have seen so far.
James is building a SmartNotes note. Every note has a title and a body. He adds a subtitle field and sets it to an empty string:
subtitle: str = ""
Emma looks at the screen. "Is an empty string the same as no subtitle?"
James pauses. "What is the difference?"
"An empty string means the subtitle exists but contains no text. None means the subtitle does not exist at all. They are different concepts. If you search for notes with subtitles, do you want to include notes where someone typed nothing into the subtitle field?"
James shakes his head. "No. I want to find notes that actually have subtitles."
Emma changes the line:
subtitle: str | None = None
The | symbol is read as "or." So str | None means "this variable holds either a string or None." It is Python's way of saying the value is optional: it might exist, or it might not.
"Now the type says exactly what you mean: this note might have a subtitle (a string), or it might not have one at all (None)."
None: No Value Yet
None is Python's way of saying "this value does not exist." It is not zero. It is not an empty string. It is not False. It is literally nothing.
# None: no value yet
middle_name: str | None = None # might not have a middle name
subtitle: str | None = None # note might not have a subtitle
Output:
>>> type(None)
<class 'NoneType'>
>>> bool(None)
False
None has its own type, NoneType. There is only one None value in all of Python. When you check it with bool(), it evaluates to False, but that does not make it the same as False:
# These are all different values
nothing: None = None # no value exists
zero: int = 0 # a number with value zero
empty: str = "" # a string with no characters
off: bool = False # a boolean meaning "no"
Output:
>>> None == 0
False
>>> None == ""
False
>>> None == False
False
Four different concepts. Four different types. None is the only one that means "this value does not exist at all."
Think of an exam. A student who writes nothing on a question and submits a blank answer (empty string "") is saying: "I saw the question but I have no answer." A student who never showed up to the exam at all (None) is saying: "I was never here." Both result in zero marks, but they mean completely different things to the teacher. "" means "present but empty." None means "absent entirely."
The Union Type: str | None
The | symbol means "or." The annotation str | None tells Python (and pyright) that a variable can hold either a string or None. This is called a union type: two possible types joined together.
# With a value
subtitle: str | None = "Advanced Techniques"
# Without a value
subtitle = None # pyright is fine: None is allowed by str | None
Output:
>>> subtitle: str | None = "Advanced Techniques"
>>> type(subtitle)
<class 'str'>
>>> subtitle = None
>>> type(subtitle)
<class 'NoneType'>
Without the | None, pyright would flag subtitle = None as an error: you promised a str, but gave it None.
The X | Y union syntax (PEP 604) requires Python 3.10+. In older code you may see Optional[str] from the typing module, which means the same thing. This course uses the modern | syntax throughout. If your workplace uses Python 3.9 or earlier, ask your team which syntax they prefer.
"The empty-string-vs-None question trips up experienced developers too. There is no universal rule. The important thing is that your type annotation documents which choice you made. If you write str | None, readers know that None is intentional, not a bug."
Type Conversions: Changing One Type to Another
Sometimes you have a value in one type and need it in another. For example, a user types "42" into a form. That input arrives as a string, but you need an integer to do math with it.
Python gives you four conversion functions, one for each type. The function name tells you what type you are converting to:
| Function | What It Does | Example |
|---|---|---|
int() | Converts to integer | int("42") → 42 |
float() | Converts to decimal | float("3.14") → 3.14 |
str() | Converts to text | str(42) → "42" |
bool() | Converts to True/False | bool(0) → False |
To use one, write the function name followed by parentheses containing the value you want to convert.
Converting strings to numbers
user_input: str = "42"
count: int = int(user_input) # 42
display: str = str(count) # "42"
ratio: float = float("3.14") # 3.14
Output:
>>> int("42")
42
>>> str(42)
'42'
>>> float("3.14")
3.14
Each conversion function takes a value and returns a new value in the target type. The original value is unchanged. int("42") reads the string "42" and produces the integer 42.
Truncation warning: int() drops decimals
rounded: int = int(3.99) # 3 (truncates, does NOT round)
also_three: int = int(3.01) # 3
negative: int = int(-3.99) # -3 (cuts toward zero, not toward -4)
Output:
>>> int(3.99)
3
>>> int(3.01)
3
>>> int(-3.99)
-3
int() always cuts toward zero: it drops everything after the decimal point. For positive numbers, that means rounding down. For negative numbers, it means rounding up (toward zero). This is different from floor division //, which always rounds toward negative infinity.
int() always cuts toward zero. It does not round. If you need rounding, use round() from Lesson 2.
bool() and truthiness rules
You saw truthiness briefly in Lesson 3. Here is the complete picture:
bool(0) # False
bool(1) # True
bool(42) # True (any non-zero number)
bool("") # False (empty string)
bool("hi") # True (non-empty string)
bool(None) # False
Output:
>>> bool(0)
False
>>> bool(1)
True
>>> bool(42)
True
>>> bool("")
False
>>> bool("hi")
True
>>> bool(None)
False
The rule: zero, empty string, None, and False are falsy. Everything else is truthy. You do not need to memorize a long list. If a value represents "nothing" (no number, no text, no value), it is falsy.
What NEVER fails: str()
str() can convert anything to a string. It never raises an error:
str(42) # "42"
str(None) # "None"
str(True) # "True"
str(3.14) # "3.14"
Output:
>>> str(42)
'42'
>>> str(None)
'None'
>>> str(True)
'True'
>>> str(3.14)
'3.14'
This makes str() the safest conversion function. When in doubt, converting to a string always works.
What Fails: ValueError
Not every conversion succeeds. When Python cannot convert a value, it raises a ValueError:
# These all raise ValueError
# int("hello") # ValueError: invalid literal for int() with base 10: 'hello'
# float("abc") # ValueError: could not convert string to float: 'abc'
# int("3.14") # ValueError: invalid literal for int() with base 10: '3.14'
The third one surprises many beginners. "3.14" looks like a number, but int() cannot handle the decimal point in a string. You must go through float() first:
# Two-step conversion: string with decimal to int
step_one: float = float("3.14") # 3.14
step_two: int = int(step_one) # 3
Output:
>>> float("3.14")
3.14
>>> int(float("3.14"))
3
In SmartNotes, user input arrives as strings from forms and text fields. If a user types "350" as the word count, you need int("350") to convert it before doing arithmetic. If they type "abc" instead, your program needs to handle the ValueError. (How to handle errors gracefully is a Phase 3 topic. For now, knowing that the error exists is enough.)
One surprise: int(True) returns 1
int(True) # 1
int(False) # 0
Output:
>>> int(True)
1
>>> int(False)
0
In Python, booleans are secretly integers. True is 1 and False is 0. This is a historical design choice, not something you need to use in practice.
PRIMM-AI+ Practice: Predicting Conversions
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
For each conversion below, predict the result. If the conversion raises a ValueError, write "ValueError." Rate your confidence from 1 (guessing) to 5 (certain) for each one.
| # | Expression | Your Prediction | Confidence (1-5) |
|---|---|---|---|
| 1 | int("100") | ||
| 2 | float("3.14") | ||
| 3 | int("3.14") | ||
| 4 | str(True) | ||
| 5 | bool("") | ||
| 6 | int(True) |
Write your predictions before opening the answer key.
Check your predictions
| # | Expression | Result | Explanation |
|---|---|---|---|
| 1 | int("100") | 100 | The string "100" contains only digits; int() converts it to the integer 100. |
| 2 | float("3.14") | 3.14 | The string "3.14" is a valid decimal number; float() converts it. |
| 3 | int("3.14") | ValueError | int() cannot handle a decimal point inside a string. Use int(float("3.14")) instead. |
| 4 | str(True) | "True" | str() converts any value to its string representation. |
| 5 | bool("") | False | An empty string is falsy. |
| 6 | int(True) | 1 | Booleans are secretly integers in Python. True equals 1. |
If you got 5 or 6 correct with confidence 4-5, your type conversion intuition is solid. If int("3.14") surprised you, that is the most common mistake in this set.
Run
Press Shift+Tab to exit Plan Mode.
Open your SmartNotes project and run each expression in Python:
uv run python -c "print(int('100'))"
uv run python -c "print(float('3.14'))"
uv run python -c "print(int('3.14'))"
uv run python -c "print(str(True))"
uv run python -c "print(bool(''))"
uv run python -c "print(int(True))"
Compare each result to your prediction. Which ones matched? Which ones surprised you?
Investigate
For each ValueError, explain WHY the conversion fails. The key question: what makes int("3.14") different from int(3.14)?
In Claude Code, type:
Why does int("3.14") raise ValueError but int(3.14) returns 3? What is the difference?
Hint: int(3.14) receives a float (a number Python already understands) and truncates it. int("3.14") receives a string (text) and tries to parse it as an integer, but the decimal point is not valid in integer text.
For int(True) returning 1: this works because Python's bool type is a subclass of int. Every True is also the integer 1, and every False is also the integer 0. This is a design choice from Python's history, not something you need to rely on in your own code.
Modify
For each expression that raised a ValueError, write a conversion that DOES work:
int("3.14")fails. Fix:int(float("3.14"))produces3.
Now try these new conversions and predict the result before running:
float("100")produces what?bool("False")produces what? (This one is tricky.)
Check your modifications
float("100")produces100.0. The string "100" is valid forfloat().bool("False")producesTrue. The string"False"is non-empty, so it is truthy.bool()checks whether the value is "something" or "nothing," not whether the text says "False." This trips up many beginners.
Make [Mastery Gate]
A user submits five inputs as strings. For each one, write the type conversion needed and predict whether it succeeds or raises ValueError:
| # | User Input | Convert To | Your Code | Succeeds or ValueError? |
|---|---|---|---|---|
| 1 | "25" | int | ||
| 2 | "hello" | int | ||
| 3 | "3.14" | float | ||
| 4 | "" | bool | ||
| 5 | "0" | bool |
Check your mastery answers
| # | User Input | Convert To | Code | Result |
|---|---|---|---|---|
| 1 | "25" | int | int("25") | 25 (succeeds) |
| 2 | "hello" | int | int("hello") | ValueError (not a number) |
| 3 | "3.14" | float | float("3.14") | 3.14 (succeeds) |
| 4 | "" | bool | bool("") | False (empty string is falsy) |
| 5 | "0" | bool | bool("0") | True (non-empty string is truthy) |
Number 5 is the hardest. The string "0" is not the integer 0. It is a non-empty string, and all non-empty strings are truthy. If you got this one right, you understand the difference between a value and its string representation.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Test Your Understanding
I'm learning about None and type conversions in Python.
Here is my understanding:
- None means "no value"
- str | None means a variable can be a string or None
- int("42") converts a string to an integer
- bool("") is False because empty strings are falsy
Is my summary accurate? What am I missing or getting wrong?
Read AI's response. Did it mention ValueError for invalid conversions? Did it clarify the difference between None and other falsy values? Compare its corrections to what you learned in this lesson.
What you are learning: You are testing your own mental model against AI's knowledge, the same pattern you will use when reviewing AI-generated code in TDG cycles.
Prompt 2: Generate a Conversion Reference
Create a Python file called smartnotes/conversions_demo.py
with typed variables that demonstrate every type conversion
from this list: int(), float(), str(), bool().
For each conversion, add a comment showing the result.
Include at least one example that would raise ValueError
(commented out with the error message).
Use type annotations on every variable.
Read the generated file. Check every comment against what you predicted. Are the results correct? Did AI handle the int("3.14") case the way you expect?
What you are learning: You are reading AI-generated code critically, verifying that its type annotations and conversion results match your understanding.
SmartNotes TDG Challenge
This is the end-of-chapter challenge. You will use everything from Lessons 1 through 4 to define typed variables for a SmartNotes note and write your first multi-variable TDG cycle. If you have the PRIMM-AI+ starter kit installed, you can run /tdg in Claude Code to guide you through each step.
Step 1: Define Typed Variables
Create a file smartnotes/note_types.py with these typed variable declarations:
title: str = "My First Note"
word_count: int = 350
reading_time: float = word_count / 250
is_draft: bool = True
subtitle: str | None = None
Five variables. Five types. Each one uses vocabulary from this chapter: str (Lesson 1), int and float with arithmetic (Lesson 2), bool (Lesson 3), and str | None (this lesson).
Step 2: Write the Function Stub and Tests
Create a file smartnotes/reading.py with the function stub:
def reading_time_minutes(word_count: int, wpm: int = 250) -> float:
"""Calculate estimated reading time in minutes."""
...
Create a file tests/test_reading.py with the tests:
from smartnotes.reading import reading_time_minutes
def test_reading_time_standard() -> None:
assert reading_time_minutes(500) == 2.0
def test_reading_time_fast_reader() -> None:
assert reading_time_minutes(500, 500) == 1.0
Step 3: Check Types
uv run pyright
Pyright should report zero errors. The stub uses ... (the ellipsis from Chapter 46), so pyright trusts the type annotations and ignores the missing body.
Step 4: Generate
Tell Claude Code:
Implement the reading_time_minutes function in smartnotes/reading.py
so that all tests in tests/test_reading.py pass.
Do not modify the tests.
Step 5: Verify
uv run pytest tests/test_reading.py -v
Both tests should pass. If they fail, re-prompt and try again.
Step 6: Read
Read the generated function body. Predict: what does reading_time_minutes(750) return? Work it out: 750 / 250 = 3.0. Does the implementation match your prediction?
Predict: what does reading_time_minutes(100, 200) return? Work it out: 100 / 200 = 0.5.
You have completed your first multi-variable TDG cycle using every type from Chapter 47.
Ch 47 Syntax Card: Primitive Types and Expressions
Keep this as a quick reference for the types, expressions, and conversions from this chapter.
# Ch 47 Syntax Card: Primitive Types and Expressions
# Type annotations
name: str = "James"
age: int = 30
price: float = 9.99
active: bool = True
middle: str | None = None
# Arithmetic
10 + 3 # 13 (int)
10 / 3 # 3.333... (float)
10 // 3 # 3 (int, floor division)
10 % 3 # 1 (remainder)
2 ** 10 # 1024 (power)
round(3.14159, 2) # 3.14
# Comparisons produce bool
age >= 18 # True
price == 0.0 # False
age >= 13 and age < 20 # False
not active # False
# Type conversions
int("42") # 42
str(42) # "42"
float("3.14") # 3.14
bool(0) # False
James reviews his Syntax Card. "int, float, str, bool, None. Five primitive types. Plus str | None for optional values, and conversion functions to move between them."
"And int('3.14') fails," Emma adds. "That one catches people. You have to go through float() first."
"I almost got tricked by bool('False') too," James says. "It returns True because the string is not empty. Python does not read the word."
Emma nods. "Good. You now have the full type vocabulary for single values. But SmartNotes needs more than one value at a time. A note has multiple tags, not just one. In Chapter 48, you learn collections: list[str] for ordered groups, dict[str, int] for key-value lookups, and more. Single values become groups of values."