This is a submission for *DEV's Summer Bug Smash: Clear the Lineup** powered by Sentry.*
Preventing Accidental Mutation of Internal State in StayPresent
Project Overview
StayPresent is an open-source Python library for running and monitoring long-running bots while exposing their status through a lightweight built-in web interface.
The library is designed around predictable behavior and reliability. Since it often runs for days or weeks at a time, seemingly small implementation details can have surprisingly large consequences.
While auditing the codebase, I discovered a subtle bug where user code could unintentionally modify the framework's own internal state.
The Bug
StayPresent provides staypresent.web.get() and staypresent.web.get_all() so applications can inspect the currently served state.
At first glance, these functions appeared to return an independent snapshot of that state.
In reality, they only made a shallow copy of the outer response object.
For JSON responses, the nested "value" object remained the exact same dictionary or list stored internally by StayPresent.
That meant code as ordinary as this:
state = staypresent.web.get()
state["value"]["count"] += 1
didn't just modify the local variable.
It silently modified the live data that StayPresent would serve to every future HTTP request.
No additional API call was required.
No warning was produced.
The framework's internal state had effectively escaped into user code.
The most concerning part was that the mutation happened completely outside the framework itself, making the resulting behavior difficult to trace back to the real cause.
The Fix
The solution was intentionally simple.
Instead of returning a shallow copy, get() and get_all() now return a deep copy of the stored state.
Every caller receives an independent snapshot that can be inspected or modified freely without affecting StayPresent's internal data.
This restores the behavior users naturally expect: reading state should never mutate it.
Why It Matters
This wasn't a crash or an exception.
The application continued running normally, but its behavior could quietly change because of an innocent-looking modification to a returned object.
Bugs like these are especially dangerous because they violate one of the most fundamental expectations of an API: that a getter doesn't expose ownership of the underlying data.
Fixing the issue not only prevents subtle data corruption, but also makes the API significantly easier to reason about.
What I Learned
This bug reinforced an important lesson: defensive copying isn't just an implementation detailโit's part of an API's contract.
When an API appears to return a snapshot, developers naturally assume they own the returned object.
If internal mutable state leaks across that boundary, seemingly harmless application code can produce behavior that's extremely difficult to diagnose.
By returning independent copies instead of shared references, StayPresent now behaves consistently with those expectations while preserving its existing public API.
Code
GitHub Repository
https://github.com/StayElite/StayPresent
Changelog (Fixed in v1.5.16)
https://github.com/StayElite/StayPresent/blob/main/CHANGELOG.md
Commit
https://github.com/StayElite/StayPresent/commit/def77e96a722de3a6705ed6dd4b654e82142adc6
Top comments (0)