Python Numbers

Numbers are one of the most fundamental building blocks in Python. Every calculation, every counter, every price or coordinate in your programs is represented using one of Python’s numeric types. Python gives you three built-in numeric types out of the box — int for whole numbers, float for decimal numbers, and complex for numbers with an imaginary component — and understanding how each one behaves internally will save you from some of the most common bugs in beginner (and even professional) code.

Overview / How Numbers Work in Python

Python is dynamically typed, which means you never declare a variable’s type explicitly. When you write x = 5, Python creates an integer object in memory and makes the name x refer to it. The type is attached to the object, not the variable name, and you can check it at any time with the built-in type() function.

Python has three built-in numeric types:

  • int — whole numbers of unlimited size, such as 7, -42, or 1_000_000_000_000.
  • float — numbers with a decimal point, such as 3.14 or -0.001, stored internally as 64-bit IEEE-754 double-precision values.
  • complex — numbers with a real and an imaginary part, written as 3+4j, used mainly in scientific and engineering code.

A crucial detail that surprises many newcomers: unlike languages such as C or Java, Python’s int type has arbitrary precision. There is no 32-bit or 64-bit overflow — an integer can grow as large as your computer’s memory allows. Under the hood, CPython implements this with a structure (PyLongObject) that stores the number as a sequence of \”digits\” in base 230, growing dynamically as needed. This is why 2 ** 1000 works perfectly fine and prints an exact, gigantic number instead of wrapping around or overflowing.

Floats, by contrast, are not arbitrary precision. A Python float is a thin wrapper around the C double type, which uses exactly 64 bits to represent a value: 1 bit for the sign, 11 bits for the exponent, and 52 bits for the mantissa (the significant digits). This fixed-width binary representation cannot exactly represent most decimal fractions, which is the root cause of the infamous 0.1 + 0.2 != 0.3 problem explored later in this lesson.

All numbers in Python are immutable. When you write x = x + 1, Python does not modify the existing integer object in place — it creates a brand-new integer object representing the result and rebinds the name x to it. The old object, if nothing else references it, is eventually garbage collected. This immutability is what makes numbers safe to use as dictionary keys and share freely between variables without unexpected side effects.

Syntax

Numeric literals can be written in several forms:

Form Example Meaning
Integer literal 42 A whole number (int)
Negative integer -17 Whole number below zero
Float literal 3.14 Decimal number (float)
Scientific notation 2.5e3 2500.0 — exponent form
Complex literal 3+4j Real part 3, imaginary part 4
Underscore grouping 1_000_000 Same as 1000000, more readable
Hexadecimal 0x1F 31 in base 10
Octal 0o17 15 in base 10
Binary 0b1010 10 in base 10

Conversion between types uses the built-in constructor functions:

  • int(value) — converts to an integer (truncates floats toward zero, parses numeric strings)
  • float(value) — converts to a floating-point number
  • complex(real, imag) — builds a complex number

The core arithmetic operators work across int and float (Python automatically promotes an int to a float when the two are mixed):

Operator Meaning Example Result
+ Addition 5 + 2 7
- Subtraction 5 - 2 3
* Multiplication 5 * 2 10
/ True division (always returns float) 5 / 2 2.5
// Floor division (rounds down) 5 // 2 2
% Modulo (remainder) 5 % 2 1
** Exponentiation 5 ** 2 25

Examples

Example 1: The Basic Numeric Types and Operators

whole_number = 42
decimal_number = 3.14159
complex_number = 3 + 4j

print(type(whole_number))
print(type(decimal_number))
print(type(complex_number))

a = 17
b = 5

print(a + b)
print(a - b)
print(a * b)
print(a / b)
print(a // b)
print(a % b)
print(a ** b)

Output:

<class 'int'>
<class 'float'>
<class 'complex'>
22
12
85
3.4
3
2
1419857

Notice that / always produces a float (even 4 / 2 gives 2.0), while // discards the fractional part and returns an integer-valued result. The last line shows exponentiation: 17 ** 5 raises 17 to the 5th power.

Example 2: Floating-Point Precision Pitfalls

import math

x = 0.1 + 0.2
print(x)
print(x == 0.3)
print(math.isclose(x, 0.3))
print(round(x, 2))

price = 19.99
quantity = 3
total = round(price * quantity, 2)
print(total)

Output:

0.30000000000000004
False
True
0.3
59.97

This is one of the most important things to understand about floats: because 0.1 and 0.2 cannot be represented exactly in binary, adding them produces a tiny rounding error, so the result is not bit-for-bit equal to 0.3. The fix is to either round to a sensible number of decimal places with round(), or compare with a tolerance using math.isclose() instead of ==.

Example 3: Formatting, Underscores, and Conversions

population = 1_400_000_000
print(population)

text_number = '256'
as_int = int(text_number)
as_float = float(text_number)
print(as_int, as_float)

pi = 3.14159265
print(f'{pi:.2f}')
print(f'{population:,}')

seconds = 3725
hours = seconds // 3600
minutes = (seconds % 3600) // 60
remaining_seconds = seconds % 60
print(f'{hours}h {minutes}m {remaining_seconds}s')

Output:

1400000000
256 256.0
3.14
1,400,000,000
1h 2m 5s

The underscore in 1_400_000_000 is purely a readability aid — Python ignores it when parsing the literal. The format spec :.2f rounds a float to two decimal places for display, and :, inserts thousands separators. The last block shows a classic use of // and % together to break a total number of seconds into hours, minutes, and seconds.

Example 4: Arbitrary-Precision Integers vs. Limited-Precision Floats

big_number = 2 ** 100
print(big_number)
print(type(big_number))

approx = 2.0 ** 100
print(approx)

z = 3 + 4j
print(z.real, z.imag)
print(abs(z))

Output:

1267650600228229401496703205376
<class 'int'>
1.2676506002282294e+30
3.0 4.0
5.0

2 ** 100 as an int prints every exact digit because integers have unlimited precision. The same value computed as a float (2.0 ** 100) loses precision and is displayed in scientific notation because a double only has about 15–17 significant decimal digits of accuracy. The complex number example shows that .real and .imag are always floats, and abs() on a complex number returns its magnitude (here, the classic 3-4-5 right triangle gives a magnitude of exactly 5.0).

How It Works Step by Step

  • When Python parses a literal like 42, the tokenizer recognizes it as an integer and the interpreter creates a PyLongObject to hold it.
  • A literal like 3.14 is instead compiled into a PyFloatObject that stores a native C double.
  • When you write a + b, Python calls the __add__ method on the left operand’s type. If both operands are int, integer addition is performed with no precision loss. If one operand is a float, the int is first promoted to a float, and the addition happens in floating-point arithmetic — this is why mixing types can silently introduce rounding error.
  • Because numbers are immutable, every arithmetic operation returns a brand-new object rather than mutating an existing one. This is different from, say, appending to a list in place.
  • CPython caches (\”interns\”) small integers between -5 and 256 as a performance optimization, so repeated use of common small numbers doesn’t allocate new objects each time — but this is an implementation detail, not something you should rely on with the is operator.

Common Mistakes

Mistake 1: Comparing floats directly with ==.

Because of binary rounding error, code like this often fails in surprising ways:

total = 0.1 + 0.2
if total == 0.3:
    print('equal')
else:
    print('not equal')

This prints not equal, even though mathematically 0.1 + 0.2 should equal 0.3. The fix is to use a tolerance-based comparison:

import math

total = 0.1 + 0.2
if math.isclose(total, 0.3):
    print('equal')
else:
    print('not equal')

This correctly prints equal because math.isclose() allows for a small relative tolerance instead of demanding bit-for-bit equality.

Mistake 2: Confusing / and //.

In Python 3, / always returns a float, even when both operands are integers and divide evenly. Code that assumes it gets an integer back can break:

items = [10, 20, 30, 40]
middle_index = len(items) / 2
print(items[middle_index])

This raises a TypeError: list indices must be integers or slices, not float, because len(items) / 2 evaluates to 2.0, a float, and floats cannot be used as list indices. The fix is to use floor division, which returns an int when both operands are integers:

items = [10, 20, 30, 40]
middle_index = len(items) // 2
print(items[middle_index])

This correctly prints 30.

Best Practices

  • Never compare floats with ==; use math.isclose() or round both sides to a fixed precision first.
  • For money or any value requiring exact decimal arithmetic, use the decimal.Decimal class instead of float — floats are a poor fit for financial calculations.
  • Use fractions.Fraction when you need exact rational arithmetic (e.g., 1/3 exactly, not an approximation).
  • Use underscores (1_000_000) in large numeric literals to improve readability.
  • Prefer // when you explicitly want integer (floor) division, and / when you want a precise float result.
  • Use f-strings with format specs (f'{value:.2f}') for controlled, readable number formatting instead of manual string manipulation.
  • Avoid relying on integer caching/interning behavior (is comparisons on numbers) — always use == to compare numeric values.
  • Round only for display purposes; keep full precision in your calculations and round at the very end to avoid compounding errors.

Practice Exercises

  • Exercise 1: Write a program that takes a temperature in Celsius (e.g., 25) and converts it to Fahrenheit using the formula F = C * 9/5 + 32, printing the result rounded to one decimal place.
  • Exercise 2: Given a total number of minutes (e.g., 135), use // and % to compute and print the equivalent number of hours and remaining minutes (expected output for 135: 2h 15m).
  • Exercise 3: Write a small script that checks whether math.sqrt(2) ** 2 is \”equal\” to 2 using both == and math.isclose(), printing both results, and explain in a comment why they differ.

Summary

  • Python has three built-in numeric types: int (arbitrary precision), float (64-bit IEEE-754 double), and complex (real + imaginary parts).
  • / always returns a float; // performs floor division and can return an int when both operands are ints.
  • Floats cannot represent most decimal fractions exactly, so direct equality comparisons (==) are unreliable — use math.isclose() or rounding instead.
  • Integers in Python never overflow because they grow dynamically to hold arbitrarily large values.
  • All numbers are immutable; arithmetic always produces new objects rather than mutating existing ones.
  • Use decimal.Decimal or fractions.Fraction when exact precision matters, such as in financial calculations.