Gyasi Sutton, MD, MPH Physician in Training, Coder at heart. Python, R, Node, and Rust.

Manipulating time like a TimeLord with Flux

  Reading Time:

Testing is a cornerstone of robust software development, but it presents unique challenges when time is a critical factor. How do you verify that a notification is sent exactly 24 hours after an event without making your test suite wait for an entire day? How do you ensure that time-sensitive logic is immune to the small, unpredictable delays of real-world execution? Mocking Python's built-in time module can quickly become a tangled mess of patched calls and fragile tests.

This is precisely where Flux comes in. Flux is a Python library designed to give you complete and deterministic control over time in your tests. It allows you to create virtual timelines, making it trivial to fast-forward, freeze, and schedule events without the wait. In this article, we'll explore how Flux works, dive into real-world use cases, and walk through how to replace unwieldy, time-based tests with elegant, fast, and reliable ones.


A Bird’s-Eye View of Flux

Flux is all about giving you full control over time in your Python tests and simulations. In a nutshell, it:

  1. Provides a virtual clock for your code to run against.
  2. Allows you to schedule callbacks to fire at specific points in the virtual timeline.
  3. Supports time factors, letting you speed up or slow down how quickly the virtual timeline progresses relative to real-world time.
  4. Optionally offers a global “current timeline” that can transparently replace calls to time.sleep() or time.time() across different modules.

This means you can test things like “what if my function runs for a whole day?” without waiting 24 real hours. You can also freeze time for deterministic tests or accelerate time to watch processes play out in a fraction of their usual duration.


Real-World Use Cases

Here are just a few places where Flux can make your life easier:

Unit Testing Long Waits

  • Code that triggers an alert after an hour/day/week can be tested immediately by fast-forwarding the virtual clock.

Accelerating Simulations

  • Got a simulation that’s meant to run for days? Crank up the “time factor” so an hour of virtual time passes in just seconds of real time.

Freezing Time for Deterministic Tests

  • By setting the time factor to zero (or using freeze()), your time-based tests can make exact comparisons without worrying about the overhead of Python or other system delays.

Scheduling Automated Callbacks

  • Want a function to run automatically once the clock hits a certain timestamp? Flux’s scheduling mechanism has you covered.

Seamless Global Integration

  • With Flux, you can replace all time module calls in your entire project (or specific modules) with a single global timeline, making your application’s notion of time fully under your control.

Hello, Flux: Your First Tutorial

1. Installation

If Flux is on PyPI or you have it locally, install it with:

pip install flux

(Or adjust accordingly if you have a different installation process.)

2. Meet Timeline

Flux offers a class called Timeline. This is the hero of the story—an object that represents a virtual clock. Let’s see it in action:


from flux import Timeline

# Create a timeline instance
timeline = Timeline()

# Get the current virtual time
print(f"Current virtual time: {timeline.time()}")

At creation, the timeline starts at a default epoch (similar to the real time’s epoch). You can query it just like time.time() in standard Python.

3. Sleeping Virtually

Timeline.sleep() behaves similarly to time.sleep(), but it advances virtual time:


print(f"Before sleep: {timeline.time()}")

# Sleep 10 virtual seconds
timeline.sleep(10)

print(f"After sleep: {timeline.time()}")

By default, s>time factor = 1, so sleeping 10 virtual seconds takes 10 real seconds—unless you change the rules.

4. Changing the Time Factor

Think of this like the classic sci-fi or fantasy trope where time moves differently in another dimension—five years in Narnia might be only two minutes in the real world. The time factor in Flux gives you that exact power over your code's timeline. It determines how many virtual seconds pass per real second. For example:


timeline.set_time_factor(5)
print(f"Time factor: {timeline.get_time_factor()}")

# Now, sleeping 2 virtual seconds will only take 0.4 real seconds.
timeline.sleep(2)

This is a game-changer if you want to speed up or slow down time-dependent logic.

5. Freezing Time

Sometimes, you don’t want the timeline to progress automatically at all. This is where freezing comes in. When time is frozen (by setting the time factor to 0), timeline.sleep() is the only way to advance the virtual clock, and it does so instantly with no real-world delay.


timeline.freeze()  # Sets time factor to 0
print(f"Frozen time: {timeline.time()}")

# Sleeping will instantly advance the virtual clock with no real waiting
timeline.sleep(30)

print(f"Time after 'sleeping' on frozen timeline: {timeline.time()}")

Perfect for avoiding flaky tests caused by unpredictable real-world time offsets.

6. Scheduling a Callback

Timeline.schedule_callback(when, callback_function) allows you to set a future point in the virtual timeline to run a function. If you need to wait until all scheduled callbacks have triggered, the library provides a convenient sleep_wait_all_scheduled() method.


def say_hello():
    print(f"Hello at virtual time: {timeline.time()}")

timeline.schedule_callback(timeline.time() + 50, say_hello)

timeline.sleep(60)  # The callback fires after we've passed the scheduled time

Seven Practical Code Snippets

Let’s walk through how Flux shines in typical situations you might face.

1. Testing a Long-Running Function

Ever needed to test that a function notifies someone after a day’s wait? With Flux, no problem:


import time

def long_running_func(_sleep=time.sleep, _time=time.time):
    start = _time()
    while True:
        if _time() - start > 60 * 60 * 24:  # 24 hours
            print("Notification triggered!")
            break
        _sleep(30)

# Test using a virtual timeline
from flux import Timeline

def test_long_wait():
    timeline = Timeline()
    timeline.set_time_factor(0)  # freeze time to skip real waits

    # After 24 hours + 1 second in virtual time, we expect a notification
    timeline.schedule_callback((60 * 60 * 24) + 1, lambda: None)

    # Run the function with the timeline's time and sleep
    long_running_func(_sleep=timeline.sleep, _time=timeline.time)

    print("Test passed without waiting 24 real hours!")

Notice how we never truly wait 24 hours in real-time. The virtual clock leaps to the future instantly.

2. Speeding Up a Simulation

Simulations often need to “hurry up” in test or demo mode. Enter time factor:


def simulate(duration):
    print("Simulation started.")
    time.sleep(duration)
    print("Simulation ended.")

from flux import Timeline, current_timeline

def test_simulation_speed():
    # Use the global timeline for convenience
    current_timeline.set(Timeline())
    current_timeline.set_time_factor(1000)

    # If we call simulate(3600), it should take around 3.6s in real time
    simulate(3600)

    print("Sim finished quickly!")

3. Scheduling Periodic Tasks

Want to “ping” a sensor every 10 virtual seconds, three times in total?


def sensor_ping():
    print("Sensor ping at virtual time")

timeline = Timeline()
timeline.set_time_factor(1)  # Normal speed (just for illustration)

# Schedule pings
for i in range(3):
    timeline.schedule_callback(timeline.time() + 10*(i+1), sensor_ping)

# Sleep long enough for all pings
timeline.sleep(40)

print("All sensor pings done!")

4. Freezing Time for Precise Assertions

When time is unfrozen, your code typically runs a few milliseconds slower or faster than you expect. Flux can remove that uncertainty:


timeline = Timeline()
timeline.freeze()

start = timeline.time()
timeline.sleep(10)
end = timeline.time()

# Perfectly deterministic test
assert (end - start) == 10, "Time delta should be exactly 10!"

print("Freeze test successful!")

5. Using the Global Timeline Across Modules

In a large project, you might want multiple modules to share the same timeline. The current_timeline proxy is perfect for this. It allows multiple modules to share the same virtual clock without having to pass the Timeline object around. Here’s the pattern:


# moduleA.py
try:
    from flux import current_timeline as time
except ImportError:
    import time # fallback in case flux is not installed

def do_something_after_a_while():
    time.sleep(100)
    print("Done!")

# test_moduleA.py
from flux import Timeline, current_timeline
import moduleA

tl = Timeline()
tl.freeze() # Instantly advance time
current_timeline.set(tl)

moduleA.do_something_after_a_while()
# "Done!" will print immediately

6. Simulating a 5-Year Stock Prediction Algorithm

The true power of Flux shines when testing complex systems where the outcome isn’t predictable by a simple formula. Imagine you've built a sophisticated stock prediction algorithm. You need to test how it performs over five years of simulated market data, a process that would be impossible to run in real-time. With Flux, you can validate its long-term behavior in minutes.


import time
import random

def run_prediction_algo(market_data, duration_days):
    """
    A complex function that simulates running a prediction algorithm.
    The logic here would be your proprietary model.
    """
    print("Starting 5-year market prediction simulation...")
    for day in range(duration_days):
        # Simulate complex daily processing
        # e.g., fetching data, running models, making trades
        _ = [random.random() ** 2 for _ in range(1000)] 
        time.sleep(86400) # Advance one virtual day

    print("Simulation complete.")
    return "final_portfolio_value"

# --- Test File ---
from flux import Timeline, current_timeline

def test_five_year_simulation():
    # Set up a global timeline that is frozen to run instantly
    tl = Timeline()
    tl.freeze()
    current_timeline.set(tl)

    five_years_in_days = 365 * 5
    
    # Run the five-year simulation. Since the timeline is frozen,
    # the 1825 calls to sleep(86400) happen instantly.
    result = run_prediction_algo(
        market_data="mock_data_source",
        duration_days=five_years_in_days
    )

    # The entire 5-year test completes in a fraction of a real second
    print(f"Test finished. Result from 5-year simulation: {result}")
    assert result == "final_portfolio_value"

test_five_year_simulation()

7. Testing a Cron-Like Scheduler

How do you test a function that's supposed to run on a complex schedule, like "every second Friday of the month"? Writing a test that waits for real Fridays to pass is not an option. This is a perfect use case for Flux, pairing it with Python's `datetime` module to check the logic of the scheduler.


import datetime
import time

# --- The Function to Test ---
def cleanup_job_runner(get_time):
    """
    A runner that executes a cleanup job, but only on the 2nd Friday of any month.
    """
    last_run_day = -1
    jobs_fired = 0
    
    # Run for a simulated year
    for _ in range(365):
        now = datetime.datetime.fromtimestamp(get_time())
        
        # Prevent running multiple times on the same day
        if now.day == last_run_day:
            time.sleep(86400) # Move to the next day
            continue

        # Logic: Is it Friday? And is it in the second week of the month?
        is_friday = (now.weekday() == 4)
        is_second_week = (now.day > 7 and now.day <= 14)

        if is_friday and is_second_week:
            print(f"Job fired on {now.strftime('%Y-%m-%d')}, the 2nd Friday.")
            jobs_fired += 1
        
        last_run_day = now.day
        time.sleep(86400) # Move to the next virtual day
        
    return jobs_fired


# --- The Test File ---
from flux import Timeline, current_timeline

def test_cleanup_job_fires_correctly_over_one_year():
    # Start on a known date: Jan 1, 2024 (a Monday)
    tl = Timeline(start_time=1704067200)
    tl.freeze()
    current_timeline.set(tl)

    # Run the scheduler function over a virtual year
    fire_count = cleanup_job_runner(get_time=current_timeline.time)

    print(f"Test complete. The job fired {fire_count} times in a virtual year.")
    
    # There are 12 months, so the job should fire 12 times in a year.
    assert fire_count == 12

test_cleanup_job_fires_correctly_over_one_year()

A Data Engineer's Perspective: Timezones and Data Synchronization

As a data engineer, one of the most persistent challenges is managing time across distributed systems and diverse data sources. While Flux brilliantly solves the problem of testing time-dependent logic within an application, the real world often introduces a far more chaotic variable: timezones.

Different databases, APIs, and services often have their own idea of what 'now' is. Cloud-based providers are notorious for this, frequently defaulting to regional timestamps (like PST or EST) depending on where a server is located. This can lead to maddening bugs where data seems to arrive out of order or disappears entirely, simply because timestamps aren't being compared on a level playing field.

This is why it is an iron-clad best practice to standardize all time data to Coordinated Universal Time (UTC). By converting every timestamp to UTC as early as possible in your data pipeline, you create a single source of truth. This practice eliminates ambiguity and ensures that when you compare timestamps from a database in Virginia and a log file from Singapore, you're comparing apples to apples. While Flux helps you control time inside your tests, a strict UTC-first policy will help you tame it in your production systems.


Final Thoughts

Flux transforms the way you handle time-based tests and simulations. By mocking time progression, you sidestep the messy patching of Python’s time module in scattered places. You can freeze time, fast-forward through days in a blink, or schedule future events with simple, expressive code.

Key Takeaways

  1. Easy Setup: A single Timeline object does everything you need.
  2. Full Control: Adjust the time factor, freeze time, or schedule callbacks.
  3. Save Time: Test day-long processes in mere seconds (or instantly!).
  4. Reduce Flakes: Eliminate the uncertainty of real-world timing.
  5. Seamless Integration: Use the global timeline to unify time usage across your codebase.

With Flux, you’ll find testing long or complex time-based scenarios becomes almost trivial—no more waiting overnight for tests to pass, and no more dealing with frustrating “close enough” validations in your assertions.

So the next time you have a feature that waits hours or days to do something, consider Flux. Your future self (and your test suite) will thank you.

SQL Alchemy for pythonic pipelines

I've primarily been developing code, benchmarks, and data tables in Python, as platforms like Snowflake often present quirks and limitations in data processing that make it...