When I first started programming, I assumed that working with decimal numbers would be very straightforward. However, that assumption quickly changed when I realized that 0.1 + 0.2 in Python isn’t exactly equal to 0.3.
This unexpected behaviour could create important issues when calculating interest rates in a banking application, processing scientific data from sensors, or trying to split a restaurant bill. That’s why it’s very important to understand how Python handles decimal numbers from the very beginning of your programming journey.
In this article, we’ll explore why computers can struggle with decimal numbers, how Python manages this issue, and how to write code that handles floating point numbers correctly.
Table of Contents
Understanding Floating Point Numbers
Best Practices to Handle Floating Point Numbers
Working with Floating Point Numbers in Practice
Understanding Floating Point Numbers
Back when I started, I found it surprising that computers can’t represent all the numbers that we, as humans, use on a daily basis. When I was working on a simple learning project that calculated discounts for an online store, I quickly ran into this limitation. I kept getting slightly “off” results for what seemed very straightforward calculations.
After looking into this weird behavior, I learned that computers have a finite amount of memory, so they can’t store infinite decimal places. Instead, they use a system called floating point representation. That’s why a seemingly easy and straightforward calculation can throw unexpected results.
# Floating point numbers might not be exact decimal_number = 0.1 # Some calculations might surprise you result = 0.1 + 0.2 print(result) # Outputs: 0.30000000000000004 |
In Python, float is the built-in type for representing decimal numbers in our code. Every time you write a number with a decimal point, you’re creating a float.
As you know, a fraction such as ⅓ can also be represented in decimal form as 0.333333… It’s easy enough for us to understand that the dots at the end indicate that the threes’ go on indefinitely.
However, computers face a challenge with this. They need to actually store these numbers using a finite amount of memory. That’s why Python uses the IEEE-754 standard, which provides a way to represent decimal numbers in binary format, even though not all numbers can be represented exactly this way.
This limitation becomes obvious if we go back to our example above. When we add 0.1 and 0.2, you might expect the result to be 0.3. However, Python may output something like 0.30000000000000004. This happens because neither 0.1 nor 0.2 can be represented exactly in binary.
To have a better understanding of what’s actually happening under the hood, let’s take a look at the binary representation of 0.1 using Python’s decimal module. This module provides tools for decimal floating point arithmetic when precise calculations are needed:
# Inspect the binary representation of 0.1 import decimal decimal_0_1 = decimal.Decimal(0.1) print(f”Exact representation of 0.1: {decimal_0_1}”) |
Running this code will show that 0.1 is stored as something like:
Exact representation of 0.1: 0.1000000000000000055511151231257827021181583404541015625 |
This result shows how binary floating-point arithmetic can only approximate certain numbers. For example, in binary, 0.1 is represented as an infinite repeating fraction (0.000110011001100110011), similar to how ⅓ repeats in decimal form.
These small inaccuracies might seem trivial but can lead to important issues in applications where precision is important. Financial calculations and science computations are particularly vulnerable, especially when these errors accumulate over iterations in loops.
Best Practices to Handle Floating Point Numbers
After running into floating point precision issues in various projects, I’ve learned several reliable ways to handle them. Let’s take a look at the most practical solutions that you can use in your everyday coding.
Formatting for Display with f-strings
Using f-strings is one of the most versatile ways to ensure numbers are displayed cleanly and appropriately. They allow you to customize formatting for decimals, percentages, alignment, and more.
price = 19.9915 discount = 0.12345 # Format price with 2 decimal places – perfect for money values formatted_price = f”${price:.2f}” # “$19.99” # Show discount as a percentage discount_percent = f”Discount: {discount:.1%}” # “Discount: 12.3%” # Align numbers in tables or columns table_price = f”{price:>10.2f}” # ” 19.99″ left_price = f”{price:<10.2f}” # “19.99 “ # Scientific notation for very large or small numbers scientific = f”{price:.2e}” # “2.00e+01” |
As you can see, each format specifier serves a specific purpose. The .2f tells Python to show exactly two decimal places, the .1% converts the number to a percentage with one decimal place and the >10 and <10 create spaces for aligning numbers, which is especially helpful when creating reports or displaying data in columns within a table.
Comparisons with math.isclose()
When comparing floating-point numbers it’s better to avoid using the == operator directly as it’s likely to create precision issues. Instead, it’s usually better to use Python’s math.isclose() function, which allows us to determine if two numbers are “close enough”.
This is generally “good enough” for pretty much all general-use cases you might encounter in your programming journey.
import math x = 0.1 + 0.2 # This equals 0.30000000000000004 # Better avoid doing this print(x == 0.3) # False (This is not what we want!) # Do this instead print(math.isclose(x, 0.3)) # True (much better!) # For stricter comparisons, adjust the tolerance print(math.isclose(x, 0.3, rel_tol=1e-10)) # True |
The math.isclose() function is aware of the fact that floating point numbers might not be exactly equal, but it checks if they are close enough for practical purposes.
You can adjust how strict this comparison should be using the rel_tol parameter.
Rounding with round()
For basic calculations where you just need to control decimal places, the round() function can be a quick and effective solution.
Imagine we are conducting an experiment in a physics lab where we measure the average velocity of an object. Some sensors can provide very precise readings so in certain scenarios we might want to round the final result to a reasonable number of decimal places before we perform further calculations.
# Distance traveled (in meters) distance = 123.456789 # Time taken (in seconds) time_taken = 9.876543 # Calculate velocity velocity = distance / time_taken # Very precise value # Round velocity to 3 decimal places before further calculations rounded_velocity = round(velocity, 3) # Simulating further calculations (e.g., predicting future positions) future_distance = rounded_velocity * 5 # Predict position after 5 seconds # Display results print(f”Measured velocity: {rounded_velocity} m/s”) # Rounded for readability print(f”Predicted distance after 5s: {future_distance} m”) # Uses rounded value |
The round() function is very useful for intermediate calculations where you’d like to control the precision before displaying or storing results.
Using the Decimal Module for Precision
When working on applications where absolute precision is necessary, it’s better to use Python’s decimal module. With it we can represent numbers as exact decimals and we avoid the inherent imprecision of floating-point numbers.
from decimal import Decimal, ROUND_HALF_UP # Create exact decimal values (always use strings!) price = Decimal(‘19.99’) tax_rate = Decimal(‘0.08’) # Calculations are now exact tax = price * tax_rate total = price + tax # Control rounding precisely final_total = total.quantize(Decimal(‘0.01’), rounding=ROUND_HALF_UP) print(f”Total with tax: ${final_total}”) # Exactly $21.59 |
I encourage you to try this code example yourself. Notice how with this module we create decimal numbers using strings. In this case, Decimal(‘19.99’) makes sure the value is stored precisely, avoiding binary floating-point errors.
The quantize method lets us specify exactly how many decimal places we want, and ROUND_HALF_UP ensures consistent rounding behavior that matches what we expect in financial calculations.
Now that you’ve seen that computers are actually able to store numbers in decimal representation, you might wonder why that’s not the standard. It all comes down to efficiency. Computer hardware has it way easier handling binary so it’s better to base everything around it and have decimal arithmetic available when needed.
Working with Floating Point Numbers in Practice
We’ve seen the issues we can face with floating point numbers and learned several reliable ways to handle them in our code. We’ve also seen the tools Python gives us to manage these issues effectively.
Let’s now take a look at a couple of practical examples to show you how to combine the best practices to solve common challenges with decimal numbers.
Common Operations with Floating Point Numbers
As we’ve seen before, the way floating-point numbers are stored in binary can cause tiny inaccuracies in even basic arithmetic operations. Let’s take a look at an example:
# A factory is measuring the length of different metal rods rod1 = 0.1 rod2 = 0.2 rod3 = 0.3 total_length = rod1 + rod2 + rod3 print(“Total length:”, total_length) # Might output 0.6000000000000001 instead of 0.6 |
In this example, we are performing a simple sum of three numbers that look simple in decimal (0.1, 0.2, and 0.3) but that cannot be represented exactly in binary.
The output is then slightly off from what we would expect. When really small inaccuracies don’t make a difference, the best approach is to use f-strings with .2f to format our output to 2 decimal places.
Something like this would ensure a clean output:
print(f”Total length: {total_length:.2f}”) # Ensures clean output: 0.60 |
Remember, though, that this is only useful for display purposes, the stored number is not modified in any way with f-strings.
Comparing Floating Point Numbers Safely
Floating point comparisons can be very tricky. As a general rule, it’s better to avoid using the == operator when dealing with floats as that can lead to head-scratching situations caused by tiny rounding errors.
Let’s take a look at this example:
import math # Account balance before withdrawal starting_balance = 7.35 # Withdrawal amount withdrawal = 3.2 # Expected remaining balance expected_balance = 4.15 # Computed balance computed_balance = starting_balance – withdrawal # Direct comparison fails if computed_balance == expected_balance: print(“Balance is correct!”) # This will NOT print else: print(“Warning: Balance mismatch!”) # This will print! # Show actual stored values print(f”Expected: {expected_balance}, Computed: {computed_balance}”) |
The expected output for this is:
Warning: Balance mismatch! Expected: 4.15, Computed: 4.1499999999999995 |
This happens because neither 7.35 nor 3.2 can’t be stored exactly in binary. This means that 7.35 – 3.2 turns out to be slightly less than 4.15.
Comparing directly using the == operator in this case fails because the computed value is close but not exactly the same as the expected value.
To overcome this issue, we could use math.isclose() instead:
if math.isclose(computed_balance, expected_balance, rel_tol=1e-9): print(“Balance is accurate!”) # This will print! |
Usually the comparisons that math.isclose() perform are good enough for most practical purposes. However, in financial applications where precision is extremely important a better approach is to use Python’s decimal module.
As we’ve seen above, with this module, decimal values are stored exactly so we can prevent rounding errors that could lead to incorrect financial reports or accounting discrepancies.
Let’s see what the previous example would look using the decimal module instead:
from decimal import Decimal, ROUND_HALF_UP # Define decimal values for financial calculations starting_balance = Decimal(“7.35”) withdrawal = Decimal(“3.2”) # Correct subtraction remaining_balance = starting_balance – withdrawal # Ensuring rounding to 2 decimal places (standard for financial operations) rounded_balance = remaining_balance.quantize(Decimal(“0.01”), rounding=ROUND_HALF_UP) # Display results print(f”Corrected Balance: ${rounded_balance}”) # Outputs: $4.15 |
For most cases math.isclose() is usually enough but if you’re handling financial transactions, accounting, or currency, it’s better to always use the decimal module. This way you can be sure you’re avoiding rounding errors.
Wrapping Up
When working with floating point numbers, remember that computers can’t represent all decimal numbers exactly so seemingly simple calculations can produce head-scratching results in Python.
To work effectively with floating point numbers in Python, you only have to keep in mind the following essential practices:
- Use f-strings with .2f to display clean, readable numbers
- Avoid using the == operator, use math.isclose() instead
- Use round() for intermediate calculations when absolute precision isn’t needed
- Use the Decimal module when absolute precision is required (financial calculations, for example)
Always keep in mind that different applications have different precision needs. What works for a game engine might not work for financial software, for example. Whatever your use case, it’s important to always test your calculations thoroughly and document your precision requirements.
If you want to learn more, the official Python documentation provides excellent resources, particularly the decimal module documentation for financial applications.
Also, make sure to check out Udacity’s Introduction to Programming Nanodegree to build a solid foundation, or take your skills to the next level with the Intermediate Python Nanodegree program, where you’ll master these and other essential Python concepts through hands-on projects.