Python Primer·Chapter 4
Values, Types and Names
Numbers, text, true and false, and nothing. What a variable really is in Python, why some values can be changed and others cannot, and the one behaviour that surprises every beginner exactly once.
This part of the book covers the Python you will use constantly in data work. It is not a complete tour of the language. It is the subset that comes up every day, explained properly, so that nothing later feels like magic.
We start with values and names. It is the least glamorous place to begin, and a misunderstanding here will quietly cost you afternoons for years, so it is worth getting right while nothing is at stake.
Names Are Labels
In Python, a name is a label tied to an object. Assignment does not create a container and it does not copy anything. It points a label at something that already exists.
That has a consequence you will meet within the hour.
x = [1, 2, 3]
y = x # NOT a copy — a second label on the same list
y.append(4)
print(x) # [1, 2, 3, 4] ← x changed too
flowchart LR
X["name: x"] --> L["list object<br/>[1, 2, 3, 4]"]
Y["name: y"] --> L
style L fill:#eef1fc,stroke:#2141c8
There is one object and two labels. Change it through either label and both see the change, because there is nothing else to see.
If you want an independent copy, ask for one:
y = x.copy() # or list(x), or x[:]
y.append(5)
print(x) # [1, 2, 3, 4] — unaffected
You can check whether two names point at the same object:
a = [1, 2]
b = a
c = [1, 2]
a == c # True — same contents
a is c # False — different objects
a is b # True — same object
Use == to compare values. Use is only to check identity, and in practice almost only for None.
Mutable and Immutable
Some objects can be changed after they are created. Some cannot. This single distinction explains most of Python’s surprising behaviour.
| Type | Mutable? | Example |
|---|---|---|
int, float |
No | 42, 3.14 |
str |
No | "hello" |
bool |
No | True |
tuple |
No | (1, 2, 3) |
list |
Yes | [1, 2, 3] |
dict |
Yes | {'a': 1} |
set |
Yes | {1, 2, 3} |
Immutable does not mean the name cannot be reassigned. It means the object itself cannot be altered.
s = "hello"
s.upper() # returns a NEW string
print(s) # 'hello' — unchanged
s = s.upper() # rebind the name to the new string
print(s) # 'HELLO'
Every string method returns a new string. This is why s.upper() on its own does nothing useful, which is a mistake absolutely everyone makes once.
Lists behave the opposite way:
nums = [3, 1, 2]
nums.sort() # changes the list, returns None
print(nums) # [1, 2, 3]
result = nums.sort() # a classic bug
print(result) # None ← sort() returned nothing
The rule that resolves both cases: methods that change an object in place return None. If a method returns something useful, it made you a new thing and left the original alone.
Numbers
Python has two number types you will use.
count = 42 # int — whole numbers, unlimited size
price = 19.99 # float — decimals, about 16 digits of precision
2 ** 100 # int handles this fine, no overflow
Integers in pure Python grow as large as your memory allows. This is unusual and pleasant. Note that NumPy integers do not behave this way, which the NumPy chapter covers.
Division has two forms, and mixing them up is a common early bug:
7 / 2 # 3.5 — true division, always gives a float
7 // 2 # 3 — floor division, rounds down
7 % 2 # 1 — remainder
-7 // 2 # -4 — rounds DOWN, not toward zero
Floats cannot represent most decimals exactly, which produces the single most reported non-bug in programming:
0.1 + 0.2 == 0.3 # False
0.1 + 0.2 # 0.30000000000000004
This is not a Python flaw. It is how binary fractions work, and every language does it. The fix is to never compare floats with ==:
import math
math.isclose(0.1 + 0.2, 0.3) # True
For money, do not use floats at all. Use integers of the smallest unit, or the decimal module.
Text
Strings are sequences of characters, written with single or double quotes. Pick one style and stay consistent.
name = "Asha"
greeting = f"Hello, {name}" # f-string — the modern way to build text
F-strings are worth learning properly because you will use them constantly. Anything inside the braces is an expression:
n = 1234567.891
f"{n:,.2f}" # '1,234,567.89' thousands separator, 2 decimals
f"{0.4567:.1%}" # '45.7%' as a percentage
f"{42:>8}" # ' 42' right-aligned in 8 characters
f"{name!r}" # "'Asha'" the repr, useful for debugging
f"{n=}" # 'n=1234567.891' prints the expression and its value
That last one is a debugging gift. f"{df.shape=}" prints both the name and the value with no extra typing.
The string methods that come up daily:
s = " Delhi, India "
s.strip() # 'Delhi, India' — remove surrounding whitespace
s.strip().lower() # 'delhi, india'
s.strip().split(", ") # ['Delhi', 'India']
"-".join(['a', 'b']) # 'a-b'
"Delhi" in s # True
s.replace("Delhi", "Sydney")
strip and lower together are the workhorses of data cleaning. A surprising proportion of “these two records should have matched” comes down to a trailing space.
True, False, and Nothing
is_active = True
is_deleted = False
result = None # the absence of a value
None is Python’s way of saying “no value here”. It is what a function returns when it does not return anything. Always test for it with is:
if result is None: # correct
if result == None: # works, but not idiomatic, and can misbehave
Truthiness
Any value can be used where a condition is expected. These are all treated as false:
False, None, 0, 0.0, "", [], {}, set()
Everything else is true. This lets you write:
items = []
if items: # False — the list is empty
print("we have items")
name = ""
if not name: # True — empty string is falsy
print("no name given")
This is idiomatic and readable. But it has one trap that matters enormously in data work:
count = 0
if count: # False! Zero is falsy
print("we have a count") # never runs, even though count exists
If zero is a legitimate value, test explicitly:
if count is not None: # correct — distinguishes 0 from missing
The same trap appears with empty DataFrames, empty arrays, and any value where “zero” and “absent” mean different things. In data work they almost always do.
Checking and Converting Types
type(42) # <class 'int'>
isinstance(42, int) # True — prefer this in code
int("42") # 42
float("3.14") # 3.14
str(42) # '42'
int(3.99) # 3 — truncates toward zero, does not round
round(3.99) # 4
bool("") # False
Conversion fails loudly when it cannot work, which is good:
int("hello") # ValueError: invalid literal for int()
That error is a friend. It is telling you your data is not what you assumed, which is information you want early rather than late.
What Carries Forward
A name is a label on an object, not a container. Assignment never copies. When two names point at the same mutable object, changes through one are visible through the other.
Methods that change something in place return None. Methods that return something new leave the original alone.
Never compare floats with ==. Use math.isclose, or np.isclose for arrays.
Zero is falsy. In data work, where zero is usually a real measurement, test is not None rather than relying on truthiness.
Next: the four collection types, and how to choose between them by what the operation costs rather than by habit.