Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 2: Python as Your Scientific Calculator

Scientific Computing with Python

Learning objectives

By the end of this chapter, you will be able to:

Prerequisites


Why this chapter matters

Now that you can identify the computational environment that ran your code, it’s time to use Python as a scientific calculator. But this chapter goes far beyond simple arithmetic: a numerical result can look precise while still being an inappropriate representation, comparison, or scientific conclusion.

You’ll learn why spacecraft have crashed, why some calculations fail catastrophically, and how to write code that handles extreme scales — from the quantum level at 10-35 meters to the cosmological scale at 1026 meters. The floating-point precision issues we explore here aren’t academic exercises; they’re the source of real bugs that have corrupted simulations, invalidated published results, and caused navigation errors costing hundreds of millions of dollars.

These fundamentals build numerical intuition: the habit of asking what a calculation supports before treating its output as evidence. Every simulation, dataset, and statistical test later in the course relies on that habit.

A number line shows a real value lying between adjacent representable floating-point values, and a comparison region defined by an absolute and relative tolerance.

Figure 1:Floating-point values are finite representations. A useful comparison states the scale and tolerance that make two values close enough for the claim at hand.

What to notice: a tolerance is part of the question, not a universal repair for numbers that differ. The right choice depends on units, measurement resolution, and the consequence of calling two results equivalent.

Try this chapter

Return to the Part I guide when choosing a first-reading route.


2.1 Core: make an arithmetic calculation legible

Open a Python cell in VS Code, or an optional ipython session, and start with the basics. Python handles arithmetic operations naturally, but there are subtleties that matter for scientific work:

2 + 2 = 4
10 / 3 = 3.3333333333333335
2 ** 10 = 1024

Notice that 10 / 3 gives us 3.3333333333333335 — not exactly 1/3! This tiny imprecision at the end might seem trivial, but it’s your first glimpse into a fundamental challenge of computational science.

Operator precedence and dimensions: a source of real bugs

Python follows PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction), but relying on memorized rules causes expensive errors. Let’s see this with a real calculation:

Wrong expression: 3.43e+14 m^(5/2)/s^2 (not a velocity)
Correct velocity: 2.98e+04 m/s
That's 29.8 km/s - Earth's orbital speed!

The wrong version calculated (GM/r)(GM/\sqrt{r}) instead of (GM/r)\sqrt{(GM/r)}. The numerical magnitude is alarming, but the more decisive diagnostic comes first: its dimensions are not velocity. A physically meaningful variable name and a dimensional check can catch this mistake before comparison with a known orbital speed.

One more precedence pitfall worth memorizing: exponentiation is right-associative.

2**3**2 = 512  (this is 2**(3**2), not (2**3)**2)
(2**3)**2 = 64

Complete Arithmetic Operators

Python provides operators beyond basic arithmetic that prove essential for scientific calculations:

17 days = 2 weeks and 3 days
17 // 3 = 5
-17 // 3 = -6

Be careful with negative values:
int(-17/3) = -5 (truncates toward zero)
-17 // 3 = -6 (floors toward -infinity)

2.2 Core: finite representation and numerical agreement

Here’s where your journey gets interesting! You’re about to peek behind the curtain and see how computers really think about numbers. This knowledge is your superpower — it’s what lets you calculate trajectories with precision.

Integers: Arbitrary Precision Power

Unlike many languages, Python integers have unlimited precision — a huge advantage when dealing with enormous numbers:

Atoms in universe: 100000000000000000000000000000000000000000000000000000000000000000000000000000000
Can even square it: 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Memory usage comparison:
Small integer (42) uses: 28 bytes
Universe atoms uses: 60 bytes
Atoms squared uses: 96 bytes

This arbitrary precision is wonderful but comes with a cost — Python integers use more memory (and can be slower) than fixed-size integers in compiled languages. This is why specialized numerical libraries become essential for large datasets.

Floating-Point Numbers: The Heart of Numerical Computing

This is it — the concept that separates casual programmers from computational scientists! Don’t worry if this seems complex at first; every professional scientist had to learn these same lessons.

On the usual CPython platforms, a Python float is an IEEE 754 binary64 value: 64 bits split into sign (1 bit), exponent (11 bits), and a fraction field (52 stored bits). For normal (non-subnormal) numbers there is an implicit leading 1, so you get about 53 bits of significand precision (roughly 15–16 decimal digits). Inspect sys.float_info on an unfamiliar platform rather than assuming an implementation detail.

Understanding Bits and Precision

Before diving into floating-point challenges, let’s understand the fundamental unit of computer memory: the bit. A bit (binary digit) can store exactly one of two values: 0 or 1. Think of it as a light switch—either on or off. Everything your computer does ultimately reduces to manipulating billions of these tiny switches.

Groups of bits form larger units:

Single vs Double Precision: The 32-bit vs 64-bit Choice

Floating-point numbers come in two main flavors:

Python float: 24 bytes
Precision: 3.141592653589793

32-bit representation: 3.1415927410125732
64-bit representation: 3.141592653589793
Lost digits in 32-bit: -8.7422780126e-08

IEEE 754 Standard bit allocation:
32-bit: 1 sign + 8 exponent + 23 fraction bits
64-bit: 1 sign + 11 exponent + 52 fraction bits

The trade-offs are crucial for scientific computing:

Aspect32-bit (float32)64-bit (float64)
Precision~7 decimal digits~15 decimal digits
Largest finite magnitudeabout 3.4×10383.4\times10^{38}about 1.8×103081.8\times10^{308}
Memory4 bytes8 bytes
PerformanceHardware- and workload-dependent; may reduce memory use and help on supported acceleratorsHardware- and workload-dependent; often chosen when its additional precision is required
Common useGraphics and some machine-learning workloadsMany scientific calculations with a demanding error budget

If you want the actual limits (min/max/eps) instead of memorizing them, use numpy.finfo:

Machine parameters for float32
---------------------------------------------------------------
precision = 6   resolution = 1e-06
machep = -23   eps =        1.1920929e-07
negep =  -24   epsneg =     5.9604645e-08
minexp = -126   tiny =       1.1754944e-38
maxexp = 128   max =        3.4028235e+38
nexp =   8   min =        -max
smallest_normal = 1.1754944e-38   smallest_subnormal = 1e-45
---------------------------------------------------------------

Machine parameters for float64
---------------------------------------------------------------
precision = 15   resolution = 1e-15
machep = -52   eps =        2.220446049250313e-16
negep =  -53   epsneg =     1.1102230246251565e-16
minexp = -1022   tiny =       2.2250738585072014e-308
maxexp = 1024   max =        1.7976931348623157e+308
nexp =   11   min =        -max
smallest_normal = 2.2250738585072014e-308   smallest_subnormal = 5e-324
---------------------------------------------------------------

Python’s ordinary float is typically binary64. Array libraries may offer—or default to—other dtypes; for example, JAX commonly runs with 64-bit values disabled unless its configuration enables them. Always inspect the precision your calculation is actually using. Choose a dtype from the error and range your calculation can tolerate, then measure performance on the hardware and workload you actually plan to use.

Why 0.1 + 0.2 ≠\neq 0.3

Now that you understand bits and precision, let’s see why even 64-bit floats can’t exactly represent simple decimals.

0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3? False
Actual stored value: 0.30000000000000004

What Python actually stores:
As exact decimals (from strings): 0.1, 0.2, 0.3
From floats (captures float approximation): 0.1000000000000000055511151231257827021181583404541015625
0.1 as float hex: 0x1.999999999999ap-4

Why does this happen? Just as 1/3 cannot be exactly represented in decimal (0.33333…0.33333\ldots), 1/10 cannot be exactly represented in binary. The lesson is not that every small rounding difference causes a failure; it is that a calculation needs a comparison rule connected to the decision being made.

Deep Dive: machine epsilon and local spacing

Ready for something mind-blowing? Your computer literally cannot tell the difference between some numbers that are mathematically different! This isn’t a flaw — it’s a fundamental property of finite systems.

Machine epsilon: 2.220446049250313e-16
That's about 2.22e-16

Can Python tell these apart from 1.0?
1.0 + epsilon/2 = 1.0, equals 1.0? True
1.0 + epsilon   = 1.0000000000000002, equals 1.0? False

At 1 AU distance (1.50e+11 m):
A representative binary64 spacing at this scale is about 3.32e-05 m
That's about 33.2 micrometres (~0.033 mm)

2.3 Core: choose a comparison that matches a claim

For values produced by a calculation, do not assume == expresses the scientific comparison you intend. Use a tolerance chosen for the quantity and claim. Exact equality remains appropriate when exactness is explicitly part of the contract.

Stage 1: name the claim and its scale.

Expected offset: 0.0 m/s
Measured offset: 1.00e-04 m/s
Exactly equal? False
The relevant question is whether this residual is resolvable for this measurement.

Stage 2: an absolute tolerance near zero.

True

Stage 3: a relative tolerance for a scale-dependent quantity.

True

2.4 Core: numerical hazards in scientific computing

You’ve mastered how Python stores numbers — now let’s see these concepts in action with real scientific calculations! This is where things get exciting: you’re about to learn the techniques that enable precision in spacecraft navigation, sensitive instruments, and scientific simulations.

Catastrophic Cancellation: When Subtraction Destroys Precision

Catastrophic cancellation happens when you subtract nearly equal numbers, eliminating most significant digits and leaving only a few unreliable digits behind.

Stage 1: The Problem

Direct: 1 - cos(1e-08) = 0.0
This result is completely wrong!

Stage 2: Understanding Why

cos(1e-08) = 1.00000000000000000000
1.0 =      1.00000000000000000000
At this scale, float64 cannot represent the difference between cos(x) and 1.0,
so cos(x) rounds to exactly 1.0 and 1 - cos(x) becomes 0.0.

Small-angle approximation: 5.000000e-17
Our result: 0.000000e+00
Loss relative to the small-angle scale: 100.0%

Stage 3: The Solution

Using identity: 5.000000e-17
Small-angle approximation: 5.000000e-17
The identity is algebraically equivalent to 1 - cos(x), but avoids the subtraction.
A logarithmic plot shows the relative disagreement between direct computation of one minus cosine of x and the algebraically equivalent stable identity two sine squared of x over two. At x equal to ten to the minus eight radians, direct subtraction rounds to zero while the stable form retains the result's scale.

Figure 2:The direct and stable expressions are algebraically equivalent, but their finite-precision computations disagree sharply for very small angles.

What to notice: this is a Python-produced plot, not a new plotting interface to learn yet. Its vertical axis is disagreement with the stable identity—not an external measure of physical correctness. Chapter 8 returns to the craft of making, labeling, and evaluating plots like this one.

Overflow and Underflow in Extreme Scales

Get ready to work with numbers that break normal intuition! Science deals with scales that push Python to its limits.

Stage 1: The Overflow Problem

float64 max: 1.7976931348623157e+308
Overflow from math.exp(1000): math range error
10.0**400 raised OverflowError: (34, 'Numerical result out of range')
This scalar Python operation raises instead of returning inf

Stage 2: The Solution - Working in Log Space

exp(1000) approx 10^434.3
Universe luminosity: 10^49.9 W
Under the stated toy assumption: 10^49.9 W

Core check: investigate a numerical claim

An analysis note reports: “The computed offset is 10-4 m/s, therefore the instrument has no measurable bias.” The calculation used binary64 values; the instrument resolution is 10-3 m/s; and no uncertainty model, calibration record, or independent standard is supplied.

Reference: numerical guard patterns

Defensive programming is not a catalog of universal numeric bounds. A useful guard names the quantity and units, the numerical failure it prevents, and the consequence of failure. For example, a positive mass can be checked before a square root because the physical model requires it; a near-zero denominator requires a threshold justified by the scale of the numerator and the decision the quotient supports. An arbitrary max_exp=100 is not validation.

The durable pattern is state a model-specific expectation → test it → record what happened → decide whether the result is usable. Later chapters develop convergence, bounded iteration, and test design with concrete numerical models.


Deep Dive: complex numbers for wave physics

Welcome to one of the most elegant corners of mathematics! Complex numbers might sound intimidating, but they’re actually your gateway to understanding wave physics, signal processing, and Fourier analysis. Python handles complex numbers as naturally as integers.

Complex number: (3+4j)
Real part: 3.0
Imaginary part: 4.0
Magnitude: 5.0

e^(ipi) = -1
Tiny imaginary part 1.22e-16 is just rounding error

Phase of (3+4j): 0.927 radians
That's 53.1 degrees

Guardrail: most functions in math do not accept complex numbers. If you might have complex values, use cmath (or NumPy’s complex-aware functions).

Complex numbers aren’t just mathematical abstractions — they’re essential for:


Reference: values, types, and scientific output

Now that you understand how Python represents numbers, let’s see how it manages them! Variables in Python are names that refer to objects, not containers that hold values. This distinction matters:

stellar_mass: 1.99e+30 kg
solar_mass: 1.99e+30 kg
Same object? True
Numerically equal? True

After doubling stellar_mass:
stellar_mass: 3.98e+30 kg
solar_mass (unchanged): 1.99e+30 kg

Python is dynamically typed — variables can refer to any type of object:

Type: int, Value: 42
Type: float, Value: 42.0
Type: str, Value: 42 measurements

Strings and scientific output formatting

Clear output formatting is not just aesthetics—it is scientific communication. Formatting makes a value readable, but it cannot create precision absent from the measurement, calculation, or uncertainty record.

Betelgeuse is 400 light-years away
Luminosity: 1.20e+05 Lsun

Format specifications for scientific data

F-strings support sophisticated formatting perfect for scientific output:

Fixed-point (2 decimals): 1234.57
Scientific notation: 1.23e+03
Width 10, 2 decimals:    1234.57
Thousands separator: 1,235

Star        Magnitude   Distance (ly)
-------------------------------------
Sirius          -1.46             8.6
Canopus         -0.74           310.0
Arcturus        -0.05            37.0

Type system and conversions

Python’s type system strikes a perfect balance — flexible enough for rapid exploration but strong enough to catch errors. Understanding types and conversions is crucial for handling data from any source.

Type of 3.14159: float
Is it a float? True

Converted '2.718' to 2.718

int(3.9) = 3
round(3.9) = 4

Selected math tools

Here comes the fun part — Python’s math module transforms your computer into a scientific calculator more powerful than anything that existed when we sent humans to the Moon!

pi = 3.141592653589793
e = 2.718281828459045
tau = 6.283185307179586

sin(30 deg) = 0.5000
cos(30 deg) = 0.8660

Flux ratio 100 = 5.0 magnitudes

Gamma(5) = 24.0 = 4!
Error function: erf(1) = 0.8427

exp(x)-1 naive: 0.000000000000000000e+00
expm1(x) stable: 9.999999999999999791e-17
log(1+y) naive: 0.000000000000000000e+00
log1p(y) stable: 9.999999999999999791e-17

hypot(3,4) = 5.0
sum(vals) = 1.0
math.fsum(vals) = 1.0

Remember: trigonometric functions use radians, not degrees! This is a constant source of bugs in scientific code.


Reference preview: from a checked calculation to reusable code

Congratulations! You’ve been exploring Python interactively, testing ideas and learning how numbers behave. Now let’s transform your interactive explorations into a reusable script:

The function below names its input unit, rejects an unphysical mass, and keeps one analytic scaling check beside the calculation. Those are the parts worth carrying from an interactive calculation into a reusable reference.

#!/usr/bin/env python
"""
schwarzschild_radius.py
Calculate Schwarzschild radius with proper numerical handling.

Constants below are rounded for pedagogy. For high-precision work, use a
consistent constants set (e.g., CODATA via astropy.constants).
"""

import math

def schwarzschild_radius_checked(mass_kg):
    """Calculate a Schwarzschild radius for a positive mass in kg."""
    G = 6.67e-11
    c = 2.998e8

    if mass_kg <= 0:
        raise ValueError("mass_kg must be positive")

    return 2 * G * mass_kg / c**2

# This calculation is linear in mass. A scaling check is meaningful here;
# a generic log-space branch is not needed for astrophysically plausible masses.
if __name__ == "__main__":
    test_masses = {
        "Earth": 5.972e24,
        "Sun": 1.989e30,
        "Sgr A*": 8.2e36,
    }

    for name, mass in test_masses.items():
        rs = schwarzschild_radius_checked(mass)
        print(f"{name}: Rs = {rs:.2e} m ({rs/1e3:.2f} km)")

    sun_radius_m = schwarzschild_radius_checked(test_masses["Sun"])
    twice_sun_radius_m = schwarzschild_radius_checked(2 * test_masses["Sun"])
    assert math.isclose(twice_sun_radius_m, 2 * sun_radius_m, rel_tol=1e-15)

Closing reflection

You’ve built a foundation for numerical computing. The seemingly simple act of adding two numbers opens questions about representation, units, scale, and evidence. Floating-point arithmetic is not broken—it is approximate by design. The important habit is to connect that approximation to the particular claim a calculation is meant to support.

Defensive numerical work is not a collection of magic bounds. A domain check, finite-value test, stable representation, or convergence rule earns trust only when its threshold and consequence are justified by the model, instrument, or decision. Working in log space is often useful for products spanning extreme scales; it is not a universal replacement for ordinary arithmetic.

When 0.1 + 0.2 is not exactly 0.3, the lesson is not “never trust floats.” It is to choose a comparison rule and test that fit the quantity at hand. When you calculate values over many orders of magnitude, ask whether the representation can overflow. When you subtract nearly equal values, look for an equivalent stable form. These are habits of numerical judgment.


Reference: quick return checklist

✓ Floating-point arithmetic is approximate by design — use a claim-appropriate tolerance for computed quantities; use == only when exact equality is the stated contract

✓ Machine epsilon (~2.2e-16) describes local binary64 spacing near 1.0; use an appropriate local scale or math.ulp for a specific value

✓ Catastrophic cancellation occurs when subtracting nearly equal numbers — use mathematical identities

✓ Work in log space to handle extreme scales without overflow/underflow

✓ Python integers have unlimited precision but use more memory than floats

✓ Variables are references to objects, not containers holding values

✓ A numerical guard needs a model-specific reason: quantity, units, failure mode, and consequence

✓ F-strings provide powerful formatting for scientific output

✓ Complex numbers are essential for wave physics and spectral analysis

✓ Always use radians with trigonometric functions

✓ Type conversion can lose information — always validate


Reference: quick tables

Math Module Functions

Read a call as module.function(arguments): the function’s documentation defines accepted argument types, units or domains, return value, and failure behavior. For example, math.isclose(a, b, *, rel_tol=..., abs_tol=...) takes two values plus explicitly named tolerances and returns a Boolean; it does not establish scientific validity on its own.

Mathematical Constants

import math

Arithmetic Functions

Trigonometric Functions (use radians!)

Special Functions

F-String Format Specifiers

FormatMeaningExampleResult
:.2fFixed decimalf"{3.14159:.2f}"3.14
:.2eScientific notationf"{1234:.2e}"1.23e+03
:10.2fWidth and decimalsf"{3.14:10.2f}" 3.14
:,.0fThousands separatorf"{1234567:,.0f}"1,234,567
:<10Left alignf"{'test':<10}"test
:>10Right alignf"{'test':>10}" test
:^10Center alignf"{'test':^10}" test

Type Checking and Conversion

FunctionPurposeExample
type(x)Get object typetype(3.14) →\to <class 'float'>
isinstance(x, type)Check typeisinstance(3.14, float) →\to True
float(x)Convert to floatfloat("3.14") →\to 3.14
int(x)Convert to int (truncates!)int(3.9) →\to 3
complex(r, i)Create complexcomplex(3, 4) →\to (3+4j)
round(x, n)Round to n decimalsround(3.14159, 2) →\to 3.14

Next Chapter Preview

Armed with a deep understanding of Python’s numeric types and the perils of floating-point arithmetic, you’re ready for Chapter 3: Control Flow & Logic. You’ll learn to make your code dynamic with if-statements and loops, building algorithms that can adapt to data and iterate until convergence. The numerical foundations from this chapter become essential when you’re checking whether a calculation has converged, comparing values within tolerance, or detecting numerical instabilities in your simulations. Get ready to transform static calculations into intelligent algorithms that can make decisions and repeat tasks — the essence of computational thinking.