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.