A Variable Is a Label, Not a Box
Most first explanations describe a variable as a box that holds a value. In Python that picture is wrong often enough to cause real bugs, so it is worth replacing straight away. In Python, values are objects that live somewhere in memory, and a variable is a name attached to one of them. Assignment does not copy a value into a container; it points a label at an object.
Change your mental image and two behaviours stop being mysterious. First, b = a does not duplicate anything — it attaches a second label to the same object. Second, reassigning a afterwards moves only that one label; b still points where it always did.
The id() function shows you the object's identity, which in CPython is its memory address. You will almost never use it in real code, but it is the fastest way to prove to yourself which names point at the same object. It becomes essential later when we get to lists, where two labels on one object means editing through either one changes what both see.
a = 10
b = a # b is now a second label on the SAME object
print(id(a) == id(b)) # True
a = 20 # this re-points a; it does not modify the object 10
print(a, b) # 20 10
# Names can be re-pointed at a completely different type
value = 42
value = "forty two"
value = [4, 2]
print(value) # [4, 2]
# Assign several names at once
x, y, z = 1, 2, 3
name = city = "unknown" # both names point at one string object - Python is dynamically typed — a name can point at any type — but it is strongly typed, so it refuses to guess across types.
"5" + 5is a TypeError, not"55"and not10. JavaScript would silently produce one of those; Python makes you say which you meant.
Naming Rules and the Conventions Reviewers Notice
The rules Python enforces are short: a name may contain letters, digits and underscores, may not start with a digit, and may not be one of the language's reserved keywords. Names are case-sensitive, so total and Total are two different variables — a surprisingly common source of NameError.
The conventions Python does not enforce matter more for your career. Python code uses snake_case for variables and functions, PascalCase for classes, and UPPER_SNAKE_CASE for constants. Writing userName instead of user_name runs perfectly and immediately marks the code as written by someone who learned Java or JavaScript first. In a code review or a placement test, that is a free negative signal you can avoid by habit.
- Valid:
user_name,total_2024,_internal,MAX_RETRIES - Invalid:
2nd_place(starts with a digit),user-name(hyphen is subtraction),class(reserved keyword) - Convention:
snake_casefor variables and functions - Convention:
PascalCasefor class names,UPPER_SNAKE_CASEfor constants - Avoid shadowing built-ins — naming a variable
list,str,sum,id,typeorinputmakes the real function unusable for the rest of the program
- Shadowing bites hardest with
listandstr. Afterlist = [1, 2, 3], a laterlist("abc")fails with "'list' object is not callable", and the error points at a line that looks perfectly correct. Runimport keyword; print(keyword.kwlist)to see every name the language reserves.
The Core Types
Five types cover almost everything you will write in the first half of this course. int holds whole numbers, float holds decimals, str holds text, bool holds True or False, and None is a single special object meaning "no value here".
Two details are worth knowing early. Python's int has no fixed width — it grows to whatever size it needs, so a factorial of 100 is computed exactly rather than overflowing the way it would in C or Java. And bool is technically a subclass of int, with True equal to 1 and False equal to 0. That is why sum([True, False, True]) returns 2, which is a genuinely useful way to count how many items in a list satisfy a condition.
None deserves care. It is not zero, not an empty string, and not False — it is its own object, and the correct way to test for it is if value is None, never if value == None. A function that ends without a return statement returns None, which is why forgetting the return produces the puzzling message "NoneType object is not subscriptable" several lines later.
count = 25 # int
price = 149.50 # float
name = "Priodemy" # str
is_enrolled = True # bool
middle_name = None # NoneType
print(type(count)) # <class 'int'>
print(type(price)) # <class 'float'>
# Prefer isinstance() over comparing type() directly
print(isinstance(count, int)) # True
print(isinstance(is_enrolled, int)) # True — bool is a subclass of int
# Big integers just work
print(2 ** 100) # 1267650600228229401496703205376
# Booleans count for free
marks = [45, 78, 92, 33, 61]
print(sum(m >= 50 for m in marks)) # 3 passed
# Testing for None
if middle_name is None:
print("No middle name recorded") int— whole numbers of any size:0,-17,10 ** 50float— decimal numbers stored in binary; approximate by naturestr— text in single, double or triple quotes; immutablebool— onlyTrueandFalse, both capitalisedNoneType— the single objectNone, meaning "deliberately empty"
Converting Between Types
Conversion is explicit in Python: you call the type you want as a function. int("42"), float("3.5"), str(99). This matters most immediately with input(), which always hands you a string. Ask for a user's age, forget to convert, and age + 1 fails with a TypeError while age * 2 quietly gives you the string repeated twice.
The gotcha is that int() is stricter than people expect. int("42") works. int("42.0") does not — it raises ValueError, because the string does not spell a whole number. If a text field might contain a decimal, convert through float first: int(float("42.7")), which gives 42. Note the direction of that truncation: int() chops toward zero rather than rounding, so int(4.9) is 4 and int(-4.9) is -4. When you want proper rounding, use round().
Conversion to bool follows a rule worth memorising, because Python applies it silently every time you write if something:. Zero, empty containers, empty strings and None are all falsy; everything else is truthy. That is why if my_list: is the idiomatic way to ask "does this list have anything in it".
age_text = "21"
age = int(age_text)
print(age + 1) # 22
# int() will not parse a decimal string
# int("21.7") # ValueError: invalid literal for int()
print(int(float("21.7"))) # 21 — go through float first
# Truncation, not rounding
print(int(4.9), int(-4.9)) # 4 -4
print(round(4.9), round(-4.9)) # 5 -5
# What counts as False
print(bool(0), bool(0.0), bool(""), bool([]), bool({}), bool(None))
# False False False False False False
print(bool(-1), bool("0"), bool([0]))
# True True True <- the STRING "0" and a list containing 0 are both truthy
marks = []
if marks:
print("has data")
else:
print("empty") # empty bool("False")isTrue. Any non-empty string is truthy, including"False","0"and"None". Reading a configuration file and testing the raw text this way is a classic silent bug — compare the string explicitly instead.
Floats Are Approximations
Run print(0.1 + 0.2) and Python answers 0.30000000000000004. This is not a bug in Python and it is not unique to Python — it happens in Java, C, JavaScript and your calculator's internals too. Floats are stored in binary, and just as one-third cannot be written exactly in decimal, one-tenth cannot be written exactly in binary. The stored value is very slightly off, and the error becomes visible when two of them are added.
The consequence is a rule: never compare floats with ==. 0.1 + 0.2 == 0.3 is False, and a program that gates on such a comparison will fail in a way that looks impossible when you read the code. Compare with a tolerance instead — math.isclose() exists precisely for this and handles the awkward cases better than a hand-written subtraction.
For money, do not use floats at all. Rupees and paise need exact arithmetic, and the standard library's Decimal type provides it. A common practical alternative in billing systems is to store amounts as integer paise and divide only when displaying, which sidesteps the problem entirely.
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True — compare with tolerance
# Formatting for display is a separate concern from storage
print(f"{0.1 + 0.2:.2f}") # 0.30
# Exact arithmetic for money
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3")) # True
# Note the quotes: Decimal(0.1) inherits the float's error,
# so always build a Decimal from a STRING. - Rounding a float for display with an f-string does not change the stored value — it only changes what is printed. If a total must be exact, round or convert at the point where the value is stored, not only where it is shown.
Mutable vs Immutable, and Why 'is' Is Not '=='
Every Python object is either immutable — it can never be changed after creation — or mutable. Numbers, strings, booleans and tuples are immutable. Lists, dictionaries and sets are mutable. This is not trivia; it decides what happens when you assign one variable to another, and it is behind a whole family of bugs in later lessons.
With an immutable object there is nothing to worry about. b = a gives you a second label, but since nobody can modify the object, the two names can never disagree. Any operation that looks like a change — name.upper(), x += 1 — actually builds a new object and re-points the name. With a mutable object, b = a means both names see every edit, because there is only one object.
This is also where is and == part company. == asks "do these have the same value?" and is what you want more than 99% of the time. is asks "are these literally the same object in memory?" Beginners reach for is because it sometimes appears to work — CPython reuses one object for small integers and for short strings that look like identifiers, so a is b can come out True for 256 and False for the same code with 257. That reuse is an internal optimisation, not a promise, and it can differ between Python versions or even between the REPL and a script. Use is only for None, True and False.
# Immutable: strings
s = "hello"
t = s
s = s.upper() # builds a NEW string; t is untouched
print(s, t) # HELLO hello
# Mutable: lists
list1 = [1, 2, 3]
list2 = list1 # one object, two names
list2.append(4)
print(list1) # [1, 2, 3, 4] <- list1 changed too
# A real copy
list3 = list1.copy() # or list1[:]
list3.append(5)
print(list1, list3) # [1, 2, 3, 4] [1, 2, 3, 4, 5]
# == compares value, is compares identity
print(list1 == list3) # False (different contents)
print(list1 is list2) # True (same object)
# Why beginners think `is` works on numbers
a = 256; b = 256
print(a is b) # True — CPython caches small ints
x = 257; y = 257
print(x is y) # often False — outside the cached range
# Never rely on either result. Use == for values. - The only correct uses of
isin everyday code arex is None,x is not None, and occasionallyx is True. For every other comparison, use==.
