Skip to content

Types and Variables

Every value in Python is an object with a type, and every variable is a name bound to one of those objects. Getting that model right early explains almost every surprising behaviour you will meet later.

Python integers have no fixed width. They grow to whatever memory allows, so there is no overflow and no int64 ceiling.

2 ** 1000
# => a 302-digit number, exactly correct
import sys
sys.maxsize # => 9223372036854775807 — the largest *index*, not the largest int

Integer literals accept underscores for readability and prefixes for other bases:

1_000_000 # => 1000000
0b1010 # binary => 10
0o755 # octal => 493
0xFF # hex => 255

Division is where Python differs most from C-family languages:

7 / 2 # => 3.5 true division, ALWAYS returns a float
7 // 2 # => 3 floor division
-7 // 2 # => -4 floors toward negative infinity, not toward zero
7 % 3 # => 1
-7 % 3 # => 2 the result takes the sign of the divisor
divmod(7, 2) # => (3, 1)
2 ** 10 # => 1024

Floats are 64-bit binary doubles, with all the usual consequences.

0.1 + 0.2 # => 0.30000000000000004
0.1 + 0.2 == 0.3 # => False

This is not a Python bug; 0.1 has no exact binary representation. Compare floats with a tolerance:

import math
math.isclose(0.1 + 0.2, 0.3) # => True
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

Special values and useful helpers:

float("inf"), float("-inf"), float("nan")
math.isnan(float("nan")) # => True
float("nan") == float("nan") # => False — NaN equals nothing, including itself
round(2.675, 2) # => 2.67 (2.675 is really 2.67499...)
round(0.5) # => 0 banker's rounding: ties go to even
round(1.5) # => 2

For money and anywhere exactness matters, use decimal; for exact ratios use fractions:

from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # => Decimal('0.3')
from fractions import Fraction
Fraction(1, 3) + Fraction(1, 6) # => Fraction(1, 2)

Built in, with a j suffix for the imaginary part. Rarely needed outside numeric work, but it exists.

z = 3 + 4j
z.real, z.imag # => (3.0, 4.0)
abs(z) # => 5.0

bool is a subclass of int. True is 1 and False is 0, and they behave as such in arithmetic.

True + True # => 2
sum([True, False, True]) # => 2 — a handy way to count matches
isinstance(True, int) # => True

str is an immutable sequence of Unicode code points. Not bytes, not UTF-8 — code points.

s = "café"
len(s) # => 4 characters, regardless of encoding
s[3] # => 'é'
'single'
"double" # identical; pick one per project
"""triple quoted
spans lines"""
"implicit" " concatenation" # => 'implicit concatenation'
r"C:\new\table" # raw: backslashes are literal — essential for regex
b"bytes literal" # a bytes object, not a str

You cannot modify a string in place. Every “modification” builds a new object.

s = "hello"
s[0] = "H" # TypeError: 'str' object does not support item assignment
s = "H" + s[1:] # => 'Hello' — a new string

Slicing is s[start:stop:step]start inclusive, stop exclusive. Negative indices count from the end. Out-of-range slices clamp instead of raising.

s = "abcdefg"
s[0] # => 'a'
s[-1] # => 'g'
s[1:4] # => 'bcd'
s[:3] # => 'abc'
s[3:] # => 'defg'
s[::2] # => 'aceg'
s[::-1] # => 'gfedcba' the idiomatic reverse
s[100] # IndexError
s[100:] # => '' slices never raise

The same slicing rules apply to lists, tuples, and every other sequence.

All of these return new strings; none mutate.

" pad ".strip() # => 'pad' (also lstrip, rstrip)
"a,b,c".split(",") # => ['a', 'b', 'c']
"a b c".split() # => ['a', 'b', 'c'] — no arg splits on any whitespace run
"-".join(["a", "b"]) # => 'a-b'
"Hello".lower() # => 'hello' (also .upper(), .casefold())
"hello world".title() # => 'Hello World'
"hello".replace("l", "L") # => 'heLLo'
"hello".startswith("he") # => True (also endswith; both accept a tuple)
"hello".find("z") # => -1
"hello".index("z") # ValueError
"a1".isdigit(), "ab".isalpha(), " ".isspace()
"file.txt".removesuffix(".txt") # => 'file' (3.9+; also removeprefix)
"line\n".splitlines() # => ['line']
"%05.2f" % 3.14159 # legacy formatting, still seen

Introduced in 3.6, f-strings are the default way to build strings. The expression inside the braces is evaluated at runtime.

name, count = "Ada", 3
f"{name} has {count} items"
f"{count * 2}" # any expression
f"{name!r}" # => "'Ada'" — !r calls repr(), !s calls str()
f"{3.14159:.2f}" # => '3.14'
f"{1234567:,}" # => '1,234,567'
f"{42:>8}" # right-align in 8 columns
f"{42:08b}" # => '00101010' binary, zero-padded
f"{0.256:.1%}" # => '25.6%'
f"{name=}" # => "name='Ada'" — self-documenting, 3.8+

Format specs are shared with format() and str.format(): [[fill]align][sign][#][0][width][,][.precision][type].

Python 3.12 relaxed f-string parsing (PEP 701): you may now reuse the same quote character inside, nest arbitrarily, and include backslashes.

# Valid only on 3.12+
f"{d["key"]}"
f"{'\n'.join(items)}"

str is text. bytes is binary data. Converting between them requires an encoding, and Python refuses to guess.

text = "café"
data = text.encode("utf-8") # => b'caf\xc3\xa9' — str -> bytes
len(data) # => 5 bytes for 4 characters
data.decode("utf-8") # => 'café' — bytes -> str
b"abc" + "def" # TypeError: can't concat str to bytes

Bytes are a sequence of integers 0–255:

data[0] # => 99 (an int, not a 1-byte object)
data[0:1] # => b'c'

The rule of thumb, sometimes called the Unicode sandwich: decode bytes to str at the boundary of your program, work in str internally, encode back to bytes on the way out. Files opened in text mode do this for you:

open("f.txt", "r", encoding="utf-8") # yields str
open("f.bin", "rb") # yields bytes

None is the single instance of NoneType. It means “no value” and is what a function returns when it has no return.

def f():
pass
f() is None # => True

Test with is, never ==:

if value is None: ...
if value is not None: ...

A Python variable is a name bound to an object. Assignment never copies a value; it points a name at an object.

a = [1, 2, 3]
b = a # b names the SAME list
b.append(4)
a # => [1, 2, 3, 4]

Rebinding a name affects only that name:

a = [1, 2, 3]
b = a
b = [9] # b now names a different list
a # => [1, 2, 3]

This is why mutable objects (list, dict, set, most class instances) behave differently from immutable ones (int, float, str, tuple, frozenset, bytes). With an immutable object there is no way to observe sharing, so you never notice.

x = 5
y = x
y += 1 # rebinds y to a new int object
x # => 5

id() shows an object’s identity (in CPython, its memory address):

a = [1]
b = a
id(a) == id(b) # => True
a, b = 1, 2
a, b = b, a # swap; no temporary needed
x = y = z = 0 # all three name the same object
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4] # init=[1, 2, 3], last=4
(a, b), c = (1, 2), 3 # nested
for i, (k, v) in enumerate(d.items()): ...

Every object is usable in a boolean context. These are falsy — everything else is truthy:

  • None, False
  • Zero of any numeric type: 0, 0.0, 0j, Decimal(0)
  • Empty containers: "", [], (), {}, set(), range(0)
  • Objects whose __bool__ returns False, or whose __len__ returns 0
if items: # idiomatic "not empty"
...
if not name: # idiomatic "empty or None"
...

and and or short-circuit and return one of the operands, not a bool:

0 or "default" # => 'default'
"a" and "b" # => 'b'
None or 0 or [] # => [] — the last operand when all are falsy
name = user_input or "anonymous" # common default idiom
  • == asks are these equal? and calls __eq__.
  • is asks are these the same object? and compares identities.
a = [1, 2]
b = [1, 2]
a == b # => True same contents
a is b # => False two distinct list objects

Use is only for singletons: None, True, False, and sentinel objects you create yourself.

MISSING = object() # a unique sentinel
def get(d, key, default=MISSING):
if default is MISSING:
...
type(3) # => <class 'int'>
type(3) is int # => True — exact type, no subclasses
isinstance(True, int) # => True — subclasses count
isinstance(x, (int, float)) # a tuple means "any of these"
isinstance(x, int | float) # 3.10+ union syntax works too
issubclass(bool, int) # => True

Prefer isinstance over type(x) == ...; it respects inheritance, which is almost always what you want. Better still, prefer duck typing — try the operation and handle failure — over type checks. See EAFP vs LBYL.

Constructors do the converting, and they raise rather than guess:

int("42") # => 42
int("1010", 2) # => 10 parse with an explicit base
int("ff", 16) # => 255
int("3.9") # ValueError — int() does not parse floats from strings
int(3.9) # => 3 truncates toward zero
float("1e3") # => 1000.0
str(42) # => '42'
list("abc") # => ['a', 'b', 'c']
bool("False") # => True — any non-empty string is truthy
  • int is unbounded; / always returns a float; // and % floor.
  • Floats are IEEE doubles — compare with math.isclose, use Decimal("...") for money.
  • bool is an int subclass, which makes sum(conditions) a counting idiom.
  • str holds code points; bytes holds octets; encode/decode bridge them, always with an explicit encoding.
  • Strings are immutable — join a list rather than += in a loop.
  • Variables are names bound to objects; assignment never copies.
  • == compares values, is compares identity — use is only for None and sentinels.
  • or/and return operands, not booleans, and short-circuit.