Skip to main content

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

"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."

If you are new to programming

Think of a paper form. A blank field (empty string) means someone looked at the question and left it empty on purpose. A field that does not appear on the form at all (None) means the question was never asked. Both are "nothing," but they mean very different things to the person reading the form.


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.

Experienced reader

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.

Emma's honest moment

"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. Python provides conversion functions named after the target type: int(), float(), str(), bool().

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

Output:

>>> int(3.99)
3
>>> int(3.01)
3

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
SmartNotes Connection

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]

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.

#ExpressionYour PredictionConfidence (1-5)
1int("100")
2float("3.14")
3int("3.14")
4str(True)
5bool("")
6int(True)

Write your predictions before opening the answer key.

Check your predictions
#ExpressionResultExplanation
1int("100")100The string "100" contains only digits; int() converts it to the integer 100.
2float("3.14")3.14The string "3.14" is a valid decimal number; float() converts it.
3int("3.14")ValueErrorint() cannot handle a decimal point inside a string. Use int(float("3.14")) instead.
4str(True)"True"str() converts any value to its string representation.
5bool("")FalseAn empty string is falsy.
6int(True)1Booleans 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

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)?

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")) produces 3.

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") produces 100.0. The string "100" is valid for float().
  • bool("False") produces True. 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 InputConvert ToYour CodeSucceeds or ValueError?
1"25"int
2"hello"int
3"3.14"float
4""bool
5"0"bool
Check your mastery answers
#User InputConvert ToCodeResult
1"25"intint("25")25 (succeeds)
2"hello"intint("hello")ValueError (not a number)
3"3.14"floatfloat("3.14")3.14 (succeeds)
4""boolbool("")False (empty string is falsy)
5"0"boolbool("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

Opening Claude Code

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.

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

Key Takeaways

  1. None means "does not exist." It is different from zero (a number), empty string (text with no characters), and False (a boolean). Use str | None when a value is genuinely optional.

  2. Type conversions use functions named after the target type. int() converts to integer, float() to decimal, str() to string, bool() to boolean. The original value is unchanged.

  3. str() never fails; int() and float() can raise ValueError. If the string does not look like a valid number, the conversion raises an error. The tricky case: int("3.14") fails because int() cannot parse a decimal point in a string.

  4. Truthiness has a simple rule. Zero, empty string, None, and False are falsy. Everything else is truthy. The string "0" is truthy because it is a non-empty string, not the integer zero.


Looking Ahead

You now have the full vocabulary of Python's primitive types: int, float, str, bool, and None. In Chapter 48, you move beyond single values to collections: list[str] for ordered sequences, dict[str, int] for key-value lookups, and tuple for fixed groups. James will try to store SmartNotes tags as a comma-separated string, and Emma will show him why list[str] is the better specification.