From Reading to Writing Types
James opens SmartNotes and types word_count = 350. He moves on to the next line.
Emma stops him. "What type is that?"
James looks at the screen. "It's a number. Obviously."
"Obviously to you. Not to pyright." She adds four characters after the variable name: : int. The line becomes word_count: int = 350. "Now pyright knows. Claude Code knows. And six months from now, you will know too."
James frowns. "But Python is dynamically typed. It figures out the type on its own. Why should I bother?"
Emma points at the screen. "Try typing word_count.upper(). The dot tells Python to call a built-in action on that value. .upper() is an action that converts text to uppercase, but word_count is a number, not text."
James types it. Pyright immediately underlines the line in red: Cannot access attribute "upper" on type "int". He did not even run the code. The error appeared the moment he typed it.
"That is why," Emma says. "Python figures out types at runtime. Pyright figures them out before you run anything. The annotation : int is your promise, and pyright holds you to it."
What Is a Data Type?
A data type tells Python what kind of value something is. The number 42 and the text "42" look similar on screen, but Python treats them completely differently. You can divide 42 by 2 and get 21.0. Try dividing "42" by 2 and Python raises a TypeError, because dividing text makes no sense.
Python has four primitive types that cover most basic values:
| Type | What It Holds | Examples |
|---|---|---|
int | Whole numbers | 42, 0, -7, 1000 |
float | Decimal numbers | 3.14, 0.5, -2.7 |
str | Text (strings) | "hello", "42", "" |
bool | True or False | True, False |
You already know these from Chapter 45, where you read them in code other people wrote. Now you write them yourself.
A data type tells Python what kind of value something is. The number 42 is an int (integer). The text "42" is a str (string). They look similar but Python treats them differently: you can do math with numbers but not with text. Type annotations (age: int = 30) are your way of telling Python and its tools what type you expect each variable to be.
Inspecting Types with type()
Python has a built-in function called type() that tells you the type of any value. Open a Python session or use Claude Code to run these:
type(42) # <class 'int'>
type("42") # <class 'str'>
type(3.14) # <class 'float'>
type(True) # <class 'bool'>
Output:
<class 'int'>
<class 'str'>
<class 'float'>
<class 'bool'>
Notice: 42 is an int but "42" (with quotes) is a str. The quotes make the difference. This distinction matters every time you write a type annotation.
Comments: Notes for Humans
Before writing annotations, one quick tool. Comments start with #. Python ignores everything after # on that line:
# This is a comment. Python skips it entirely.
word_count: int = 350 # This comment explains the variable
Output:
No output. Comments produce nothing; they exist for the humans reading the code. Use comments to explain why something exists, not what it does. The code itself shows the "what."
Writing Your First Type Annotations
A type annotation sits between the variable name and the = sign. The pattern is always the same:
name: type = value
Here are four variables, one for each primitive type:
name: str = "James"
age: int = 30
price: float = 9.99
is_member: bool = True
Output (from print statements):
James
30
9.99
True
Each annotation makes a promise: name will always hold a str, age will always hold an int, and so on. Pyright enforces these promises.
Variable Naming Rules
Python has rules about what counts as a valid variable name. The convention in Python is snake_case: lowercase letters with underscores separating words.
# Good names (snake_case, descriptive)
word_count: int = 350
reading_time: float = 1.4
is_draft: bool = True
user_name: str = "Alice"
Some names are invalid. Python rejects them before your code even runs:
# Invalid names (these cause errors):
# 2fast = 10 # Cannot start with a number
# my-name = "Jo" # Hyphens not allowed (use underscores)
# class = "Math" # 'class' is a reserved word
Reserved words are names Python uses for its own syntax: class, def, return, True, False, None, and, or, not, if, for, while, and others. You cannot use these as variable names.
For bool variables, start the name with is_, has_, or can_ to make the True/False meaning obvious:
is_draft: bool = True(reads as "is draft? yes")has_password: bool = False(reads as "has password? no")can_edit: bool = True(reads as "can edit? yes")
Reassignment and Type Errors
Once you annotate a variable, you can reassign it to a new value of the same type:
age: int = 30
age = 31 # Fine: 31 is still an int
Output (from pyright):
0 errors, 0 warnings, 0 informations
But if you try to assign a value of a different type, pyright catches it:
age: int = 30
age = "thirty-one" # Wrong type!
Output (from pyright):
error: Type "str" is not assignable to type "int"
This is the promise in action. You said age is an int. Assigning a str breaks that promise. Pyright does not let it pass quietly. You see the error before the code ever runs.
If you know types from Java, TypeScript, or C#, Python's annotations work similarly but are NOT enforced at runtime. They are metadata for static analysis tools like pyright. Python remains dynamically typed underneath. If you removed the annotation and ran the code, Python would accept age = "thirty-one" without complaint. Pyright is the guardrail, not Python itself.
Putting It Together
Here is a complete set of SmartNotes variables with types, comments, and one deliberate error:
# SmartNotes: variables for a single note
title: str = "My First Note"
word_count: int = 350
reading_time: float = 1.4
is_draft: bool = True
# Update the word count after editing
word_count = 420 # Same type (int), no error
# Oops: wrong type
word_count = "four hundred" # pyright: Type "str" is not assignable to type "int"
Output (from pyright):
error: Type "str" is not assignable to type "int" (line 10)
Pyright catches the error on the last line. The fix: either keep word_count as an int (use word_count = 400) or change the annotation to str (but then you lose the ability to do math with it). Types force you to decide what a variable is for.
PRIMM-AI+ Practice: Spotting Type Errors
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
Look at these three assignments. Without running anything, predict which ones pyright will flag as errors. Write your predictions and a confidence score from 1 to 5 before checking.
temperature: float = 98.6
temperature = 100
user_name: str = "Bob"
user_name = 42
is_active: bool = True
is_active = False
Check your predictions
Assignment 1: temperature = 100. Pyright does NOT flag this. Why? Because int is compatible with float in Python's type system. An integer can be used where a float is expected. This is a special rule: int is a subtype of float.
Assignment 2: user_name = 42. Pyright DOES flag this. int is not assignable to str. You promised text; you gave a number.
Assignment 3: is_active = False. Pyright does NOT flag this. False is a bool, same type as True. No mismatch.
If you predicted 1 out of 3 correctly, the int-to-float rule is the tricky one. If you predicted 2-3 correctly, your type intuition is solid.
Run
Press Shift+Tab to exit Plan Mode.
Create a file called type_practice.py with the assignments above. Run uv run pyright type_practice.py. Compare the output to your predictions.
Investigate
For each flagged assignment, write a one-sentence explanation of why pyright flagged it. Use this format: "Pyright flagged this because [variable] was annotated as [type], but [value] is a [different type]."
For the temperature = 100 case that was NOT flagged, write a sentence explaining why int is allowed where float is expected.
If you want to go deeper, run /investigate @type_practice.py in Claude Code and ask why int is compatible with float.
Modify
Fix each flagged assignment so pyright passes. There are two strategies:
- Change the value to match the annotation
- Change the annotation to match the value
Try both strategies for user_name = 42. Which fix makes more sense for a variable called user_name?
Make [Mastery Gate]
Without looking at any examples, write 5 typed variable declarations from scratch. Requirements:
- Use all four types (
int,float,str,bool) at least once - Use valid snake_case names
- Include at least one
boolvariable that starts withis_orhas_
Run uv run pyright on your file. Zero errors means you pass.
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: Check Your Understanding
I just learned about Python type annotations. Here is my
understanding: "Type annotations tell Python what type a
variable is, and Python enforces them at runtime."
Is my summary accurate? What am I getting wrong?
Read AI's response carefully. Did it correct the runtime enforcement claim? (Python does NOT enforce annotations at runtime; pyright does.) Compare its correction to what you learned in this lesson.
What you're learning: You are testing your own understanding by stating it confidently and letting AI find the gap. This is the same verify-then-refine pattern you will use when reviewing AI-generated code.
Prompt 2: Generate and Review Typed Variables
Create 6 typed Python variables for a simple book tracker:
title, author, page count, current page, price, and whether
the book is finished. Use snake_case names and type annotations.
Include a comment explaining each variable.
Read AI's output. Check: did it use the correct types? Did it follow snake_case? Does the bool variable start with is_ or has_? If anything looks wrong, tell AI what to fix.
What you're learning: You are reviewing AI-generated annotations against the rules from this lesson. This is Step 5 (Read) from the TDG loop: you verify AI's work, you do not blindly accept it.
James looks at his SmartNotes variables: title: str, word_count: int, reading_time: float, is_draft: bool. "Four types. Four promises. And pyright catches it the moment I break one."
"That is the whole point," Emma says. "You are not memorizing syntax. You are telling your tools exactly what kind of data to expect. When you get that right, pyright does the hard work of checking."
James pauses. "But all I have done is store values. I have not combined them. What happens if I divide word_count by a float? Does the result become a float too?"
"Good question. That is Lesson 2: arithmetic with types. The answer has a surprise that catches everyone the first time."