Variables & Data Types
1 · The lesson
readA variable in Python isn't a box you put a value into — it's a name bound to an object living somewhere in memory. That distinction sounds pedantic now; it will save you hours of confusion the first time two names appear to share a list.
This lesson covers the binding model, the naming rules the community agrees on, the handful of built-in types you'll meet on day one, and the difference between values you can mutate in place and values you can't.
1. Binding, Not Boxing
x = 42Python creates the integer object 42, then makes the name x refer to it. The name lives in the current scope; the object lives in memory. Reassign and the name simply points elsewhere — the old object gets garbage-collected if nothing else refers to it.
x = 42 x = "now I'm a string" # totally legal — x is just a label x = [1, 2, 3] # still legal — labels are typeless
This is dynamic typing: values have types, names don't. The type lives on the object, not the variable.
2. Naming Rules and PEP 8
Python's rules for what's a legal name:
- Letters, digits, and underscores only
- Cannot start with a digit
- Cannot be a reserved keyword (
if,for,class,lambda,True, etc.) - Case-sensitive —
scoreandScoreare different names
user_age = 30 # fine _internal = "ok" # leading underscore = "private by convention" 2nd_place = "bronze" # SyntaxError — can't start with a digit class = "Physics 101" # SyntaxError — class is a keyword
PEP 8 conventions — not enforced by the interpreter, enforced by every code review you'll ever sit in:
| Kind | Convention | Example |
|---|---|---|
| Variable, function | snake_case | user_age, parse_input |
| Constant | UPPER_SNAKE | MAX_RETRIES, PI |
| Class | PascalCase | HttpClient, User |
| "Private" | leading _ | _internal_cache |
Names should describe what the value means, not what type it is. users beats user_list. count beats n.
3. The Built-in Types You'll See Every Day
The five primitives:
age = 30 # int — whole numbers, unlimited size pi = 3.14159 # float — decimal, ~15-17 digit precision name = "Ada" # str — text is_active = True # bool — True or False (note capitals) result = None # None — "no value", Python's null
Then the four built-in collections you'll get dedicated lessons on:
scores = [88, 92, 75] # list — ordered, mutable → ../lists/ point = (3, 4) # tuple — ordered, immutable → ../tuples/ user = {"name": "Ada"} # dict — key→value mapping → ../dictionaries/ tags = {"python", "web"} # set — unique, unordered → ../sets/
Don't memorise the collections yet — each gets its own lesson. Just know they exist and recognise the literal syntax.
4. Inspecting Types: type() and isinstance()
print(type(42)) # <class 'int'> print(type(3.14)) # <class 'float'> print(type("hi")) # <class 'str'> print(type([1, 2])) # <class 'list'>
type() is fine for quick debugging. For real checks — especially inside functions — use isinstance(), which also handles inheritance correctly:
x = 42 print(isinstance(x, int)) # True print(isinstance(x, (int, float))) # True — accepts a tuple of allowed types
You'll lean on isinstance() in the Exceptions lesson when validating inputs.
5. Type Conversion
Python won't silently convert types for you. You ask, you get:
int("42") # 42 — string → int float("3.14") # 3.14 — string → float str(3.14) # "3.14" — anything → string bool(0), bool(1), bool("") # False, True, False
The trap:
int("3.14") # ValueError — int() won't parse a decimal string int(float("3.14")) # 3 — go through float first
Reading numbers from a user is where this bites first:
age = int(input("Age: ")) # crashes if they type "thirty"
You'll handle that properly in Errors & Exceptions.
6. Mutability — the Idea That Underpins Everything Later
Some objects can be changed in place. Some can't. This isn't a stylistic detail; it changes how assignment behaves.
Immutable — int, float, str, bool, tuple, None. Once created, the object can never change. Operations return new objects.
name = "ada" name.upper() # returns "ADA" — but name is still "ada" name = name.upper() # now name points to the new string "ADA"
Mutable — list, dict, set. The object itself can be modified.
scores = [88, 92, 75] scores.append(100) # mutates the existing list print(scores) # [88, 92, 75, 100]
The consequence — and this is the one that bites everyone once:
a = [1, 2, 3] b = a # b doesn't get a copy — b is another name for the same list b.append(99) print(a) # [1, 2, 3, 99] — surprise
= doesn't copy. It binds another name to the same object. To actually copy a list, use a.copy() or list(a). We'll come back to this in Lists.
Common Mistakes
- Shadowing built-ins. Naming a variable
list,dict,str, ortypehides the built-in for the rest of that scope.list = [1, 2, 3]thenlist(range(5))blows up with aTypeError. Pickitemsorvaluesinstead. - Assuming
=copies. It doesn't — both names point to the same object. Only matters for mutable types, but it matters a lot there. int(input(...))with no safety net. Fine for a tutorial; not fine for anything a real human will use. Wrap it intry/exceptonce you've read the exceptions lesson.- Confusing
NonewithFalseor0. They're all "falsy" (see Operators), but they're distinct objects.Nonemeans "no value yet";0is a number;Falseis a boolean.
🎯 Your Turn — Describe Any Value
Write a function describe(value) that prints three things:
1. The type of the value (e.g. int, str, list).
2. The value itself.
3. Its length — but only if length makes sense for that type.
Calling describe("Margaret") should print the type str, the value Margaret, and length 5. Calling describe(42) should print the type and value, and skip the length line.
Skeleton:
def describe(value): # TODO 1: print the type # TODO 2: print the value # TODO 3: print the length, but only if it has one ... describe("Margaret") describe(42) describe([1, 2, 3, 4]) describe(None)
Hint 1 — Getting a clean type name
type(value).__name__ gives you the short string "int" instead of <class 'int'>. Cleaner for printing.
Hint 2 — Checking "does it have a length?"
Not every object supportslen(). Numbers and None don't. Two options: check the type with isinstance(value, (str, list, tuple, dict, set)), or wrap the len() call in a try/except TypeError. The isinstance route is more explicit.
Show full solution
def describe(value): print(f"Type: {type(value).__name__}") print(f"Value: {value}") if isinstance(value, (str, list, tuple, dict, set)): print(f"Length: {len(value)}") print("-" * 20) describe("Margaret") describe(42) describe([1, 2, 3, 4]) describe(None)
Output:
Type: str Value: Margaret Length: 5 -------------------- Type: int Value: 42 -------------------- Type: list Value: [1, 2, 3, 4] Length: 4 -------------------- Type: NoneType Value: None --------------------
You've used type(), isinstance() with a tuple of types, len(), an f-string, and a conditional — five tools in twelve lines.
What You Learned
- A variable is a name bound to an object — not a container. Names are typeless; objects carry the type.
- PEP 8:
snake_casefor variables and functions,UPPER_SNAKEfor constants,PascalCasefor classes. - The five primitives —
int,float,str,bool,None— plus the four collections you'll meet later. type()for debugging,isinstance()for real checks.- Conversion is explicit:
int("42"),str(3.14).int("3.14")is aValueError— go throughfloatfirst. - Mutability matters.
=never copies; it only binds another name to the same object.
Next: Numbers in Depth — integer division, the floating-point trap, and the math module you'll actually use.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code actually did, not just whether a test passed.