forbiddenfruit lets Python code add methods to int and str
Patch built-in python objects
At a glance
- What is it?
- forbiddenfruit is a small Python library that patches built-in types through the C API, so an int can grow a method and a str can grow a classmethod. It suits a test framework or a profiling layer that wants an RSpec-shaped API, and it only runs on CPython.
- Who is it for?
- forbiddenfruit suits a team writing an assertion library, a test spy or a profiling layer that needs to extend built-in types, and that runs on CPython and is willing to accept a C extension in its dependency tree. It is the wrong tool for a library that must run on PyPy or Jython, because the implementation depends on the CPython C API, and it is the wrong tool for a codebase where monkey patching is already treated as a smell.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 35 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Patching built-ins through the C API
forbiddenfruit allows Python code to extend built-in types, and the mechanism is the reason the library exists. The README puts it plainly: it basically allows you to patch built-in objects, declared in C through Python.
The interface is a single function called curse, which takes a type, a name and a value. The first example adds a new method to the int class:
from forbiddenfruit import curse
def words_of_wisdom(self):
return self * "blah "
curse(int, "words_of_wisdom", words_of_wisdom)
assert (2).words_of_wisdom() == "blah blah "The second example attaches a classmethod to the str class, which shows that the value you pass is not limited to a plain function:
from forbiddenfruit import curse
def hello(self):
return "blah"
curse(str, "hello", classmethod(hello))
assert str.hello() == "blah"That second form is the more interesting one, because a classmethod on a built-in type is something pure Python cannot express at all. There is no syntax for it, because the type object itself belongs to the interpreter, and only a C-level operation can add to it. That is the boundary the library is exploiting.
The mechanism matters for a second reason. Because the attributes are added to the type object, they are visible everywhere in the process at once. A method added to int in one module is available to every other module in the same interpreter, with no import needed. That is the capability and the hazard in one sentence, and it is why the library was used the way it was in the first place.
The first use was an assertion library, and the list grew
The README is candid about how the project started, and the story explains both its design and its scope.
The first need this project attended was allowing a Python assertion library to implement a similar API to RSpec Expectations and should.js. So the original motivation was a test framework that wanted the fluent expectation syntax people know from Ruby and JavaScript, applied to Python's built-in values.
Then people got creative, and the README names two of the things that happened next. People used it to spy on things, in a file spy implementation, and to integrate profiling, in a localstack analytics profiler. The library that made an assertion API possible also became the mechanism for instrumenting objects, which is exactly the kind of drift a maintainer has to think about when deciding how permissive the API should be.
The README opens with a sentence that is worth taking literally: if that is a good idea or not, you tell me. The author is not claiming that patching built-in types is good practice. He is saying it is possible, that the code exists, and that the judgement is yours.
That framing has an important consequence for anyone evaluating the library. It is a mechanism with no opinion about its use, which means the quality question shifts from whether the design is good to whether the thing you are building can survive the fact that a process-wide mutation is in play. The reverse and context manager functions exist precisely because the author expected that question, and they are the subject of the next two sections.
reverse() deletes, and that limitation is documented
The escape hatch is the reverse function, and the README is careful to state its limit.
from forbiddenfruit import curse, reverse
curse(str, "test", "blah")
assert 'test' in dir(str)
# Time to reverse the curse
reverse(str, "test")
assert 'test' not in dir(str)The behaviour is straightforward. A curse adds an attribute and reverse removes it, and the assertion in the example confirms the attribute is gone from dir(str) afterwards.
The warning is the part to read twice. The README says beware, reverse() only deletes attributes. If you cursed to replace a pre-existing attribute, reverse will not re-install the existing attribute. The deletion removes the name, and whatever the type had under that name is gone with it, because the original was overwritten rather than shadowed.
That is a real trap in a test suite, where a library might add a method named the same as something a type already has, or where two test modules both patch the same name and only one of them reverses it. The second case is the more common failure, since the order of imports decides which patch wins and which reverse restores what.
The practical advice is to use the context manager form wherever the scope can be made explicit, because that is what it exists for.
cursed() scopes the change, as a context manager or a decorator
The cursed function makes the patch temporary. The README describes it as acting as a context manager to make a curse, and then reverse it on exit, and it uses contextlib.contextmanager, so on Python 3.2 and later it can also be used as a function decorator.
from forbiddenfruit import cursed
with cursed(str, "test", "blah"):
assert str.test == "blah"
assert "test" not in dir(str)
@cursed(str, "test", "blah")
def function():
assert str.test == "blah"
function()
assert "test" not in dir(str)Both forms are in the example, and both end with the same assertion that the attribute is gone. The decorator form is the more useful of the two in a test suite, because a test function decorated with cursed gets its patch applied for the duration of the call and removed afterwards, which means one test cannot leak a patched built-in into the next.
This is the design that makes the library tolerable in a codebase that does not otherwise like monkey patching. The unscoped curse is a process-wide mutation that stays until someone reverses it, and the scoped form turns the same operation into something with a lexical lifetime.
The implementation detail worth knowing is that the scope is enforced by a context manager rather than by any special support in the C layer, so the guarantee is that the exit path runs, not that the patch is atomic. An exception inside the block still triggers the exit, which is the behaviour contextlib gives you.
CPython only, and the C extension is optional to build
The compatibility statement is short and absolute. Forbidden Fruit is tested on CPython 3.7 to 3.13, and since it is fundamentally dependent on the C API, the library will not work on other Python implementations such as Jython or pypy.
That is the defining constraint of the project, and it is a hard one. There is no pure Python fallback, because the operation being performed, adding an attribute to a type object that the interpreter owns, is not available from Python at all. Any project that supports PyPy as a first-class target cannot use this library.
The build system shows how the C side is handled. The Makefile exports FFRUIT_EXTENSION=true, which is what switches on building the C extension, and the dist target empties that variable with a comment explaining why: the ffruit C test stub is not bundled into the released package, it is only used by the tests. So the published wheel has the extension built in, and the stub exists only to exercise the code paths during testing.
The test layout is organised in three suites. The Makefile has unit, functional and integration targets, each delegating to run_test with a suite variable, and run_test checks whether the tests directory for that suite exists before doing anything. The prepare step depends on clean, install_deps and build_test_stub, and the stub is built with python setup.py build and then the shared objects are moved into tests/unit with find.
The changelog for 0.1.5 records a feature addition, support for cursing __next__, __hash__ and __new__, and a build change to use pytest for tests and twine for publishing, plus a switch to GitHub CI instead of Travis. The 0.1.4 entry records the addition of the cursed context manager and decorator, conditional building of the test C extension, allowing dunder methods to be cursed with non functions, and a fix for dual licensing issues where both GPLv3 and MIT licence files are now distributed.
Dual licensing, and where the project stands
The licence is the one with an unusual shape. The licence field on the repository says GPL-3.0, and the README presents the software as available under two different licences at your choice, with the full GPLv3 text followed by the MIT text. The setup.py header says the same thing, that the program is dual licensed under GPLv3 and MIT, and the repository ships both COPYING and COPYING.mit at the root. The 0.1.4 changelog entry notes the fix for dual licensing issues and the distribution of both licence files.
For an adopter the practical effect is a choice rather than a constraint. A project that wants to use forbiddenfruit internally can take it under MIT without the copyleft reach of GPLv3, and a project that wants to distribute a modified version can comply with GPLv3 instead. The two files in the repository root are the documents that define the terms, and they are worth reading rather than assuming from the metadata field.
On maintenance, the facts are that the repository is not archived, the last push was on 2026-09-18, and the recent releases are 0.1.5 on 2026-09-18 and 0.1.4 on 2021-01-18. The gap between those two releases is five years, and the work that closed it is the 0.1.5 entry: dunder support, the move to pytest and twine, and the move from Travis to GitHub CI. The build badge in the README still points at a Travis CI status, which suggests the badge was not updated when the CI moved.
So the picture is a mature, small library that had a long quiet period and has just had a release that modernised its tooling and widened what it can patch. The version number is still 0.1.5, which tells you the author is not promising an API that will not move, and the project page at clarete.li/forbiddenfruit is the other place to look for the current state.
Editorial conclusion
forbiddenfruit suits a team writing an assertion library, a test spy or a profiling layer that needs to extend built-in types, and that runs on CPython and is willing to accept a C extension in its dependency tree. It is the wrong tool for a library that must run on PyPy or Jython, because the implementation depends on the CPython C API, and it is the wrong tool for a codebase where monkey patching is already treated as a smell. Before you adopt it, check the version range, since the library is tested on CPython 3.7 to 3.13, and read the reverse warning, because reverse only deletes attributes and will not restore one you replaced.
Frequently asked questions
What does forbiddenfruit let me do in Python?
It lets you add attributes to built-in types such as int and str, including methods, classmethods and dunder names like __next__, __hash__ and __new__. The change is made through the CPython C API and is visible process-wide.
Does forbiddenfruit work on PyPy or Jython?
No. The README states that Forbidden Fruit is tested on CPython 3.7 to 3.13 and that, since it is fundamentally dependent on the C API, the library will not work on other Python implementations such as Jython or pypy.
How do I undo a patch applied with forbiddenfruit?
Use the reverse function for the type and attribute name, or use the cursed context manager or decorator, which reverses the change on exit. Note that reverse only deletes attributes: if you replaced a pre-existing attribute, reverse will not re-install the original.
What licence is forbiddenfruit released under?
It is dual licensed at your choice between GPLv3 and MIT, with both the GPLv3 and MIT licence files distributed in the repository. The setup.py header states the program is dual licensed under GPLv3 and MIT.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/clarete-forbiddenfruit)Was this useful?