It's a bit like OverlayFS for Python modules - it allows you write modifications for a target module (lower) in a new module (upper), and have these combined in a new virtual module (mount).
It works by rewriting imports using AST transformations, then running both the lower and upper module's code in the new Python module.
This prevents polluting the global namespace when monkey-patching, and means if you want to make changes to a third-party package, you don't have to take on the maintenance burden of forking, you can package and distribute just your changes.
Edit: okay Readme is clear on it and the description does make sense, the short description here just confused me.
When you have a scalpel, you give it to operating doctors during the operation, not to 5 year olds on the street.
If I control all the imports I can usually subclass things myself just fine.
This seems to explicitly handle the case you are interested in - automatically updating library-internal references to the lower to instead use the upper?
If A is my application, B is buggy, and C is some other library, consider:
# A.py
monkeypatch_B()
import C
# C.py
B = __import__('B')
# B.py
bugs()
> * Fix bugs in third-party libraries without forking
> * Modify the behavior of existing functions
> * Add new features or options to existing classes
> * Test alternative implementations in an isolated way
only the last sounds close to something you might actually want to do, and then only as a throwaway thing
If you want to change a library, fork it. If you want to change the behavior of existing functions, don't or at least fork first. If you want to add new features to a class, write a new class, or again, at least fork first
Point being, it's a lot of really complicated fiddling with the python import system. And a lesson I have learned is that messing around with import internals in python is extremely tricky to get right. Furthermore, trying to coordinate correctly between modules that do and don't get modified my the hook is very finicky. Not to mention that supply side attacks on the import system itself could be a terrifying attack vector that would be absurdly difficult to detect.
All this to say, I'm not a big fan of monkeypatching, but I know exactly how it behaves, its edge cases, and what to expect if I do it. It is, after all, pretty standard practice to patch things during python unit tests. And even with all its warts, I would prefer patching to import fiddling any day of the week and twice on Sunday.
Feedback for the author: you need to explain the "why" of your project more thoroughly. I'm sure you had a good reason to strike out in this direction, and maybe this is a super elegant solution. But you've failed to explain to me under what circumstances I might also encounter the same problems with patching that you've encountered, in order to explain to me why the risk of an import hook is justified.
> means if you want to make changes to a third-party package, you don't have to take on the maintenance burden of forking, you can package and distribute just your changes.
That's a big win. I've seen and done my share of `# this file from github.com/blah with minor change X to L123` etc.
I've done my fair share of that too, but I'm still not seeing the benefit vs patching.
The README mentions 3 scenarios that this might be preferred over, but not the fourth which I regularly do: Create my own functions/classes that are composed from the unchanged modules. E.g. a request_with_retries function which adds retry logic to requests without the need to monkey patch. I regularly use decorators as well to add things like retries.
For more complex scenarios Modshim might win out, as mentioned in the understated section of the README "Benefits of this Approach":
> Internal Reference Rewriting: This example demonstrates modshim's most powerful feature. By replacing requests.sessions.Session, we automatically upgraded top-level functions like requests.get() because their internal references to Session are redirected to our new class.
> Preservation of the Original Module: The original requests package is not altered. Code in other parts of an application that imports requests directly will continue to use the original Session object without any retry logic, preventing unintended side-effects.
What I think this means is Modshim lets you really get in to the guts of a module (monkey-patch style, giving you god-like powers), while limiting the damage.
The big win here is that it keeps things clean and maintainable — you only ship your changes instead of managing a full fork, and you don’t mess up the global namespace. It also makes experimenting with tweaks a lot easier.
The tricky parts might be keeping import behavior consistent and making sure debugging still works nicely since AST rewriting can sometimes make stack traces a bit messy.
Overall, it’s a clever middle ground between monkey-patching and forking — really nice concept.
Your patch "with retries" might never be accepted, and maintaining any kind of fork(s) or "out-of-tree patches" is not as integrated into the programming environment. Being able to say "assert WrappedLoginLibrary().login(), '...with retries...'" keeps you testable and "in" the language proper.
This solution is interesting, as it provides the patched code as if it were a new package, indendant of the existing one you have installed, like vendoring, but without the burden of it.
In case you want to be the only one seing your patch, this is great. It also makes the whole maintenance easier, as you don't have to wonder if you patch it at the right time or in the right way. MK can fail in many subtle edge cases.
Inheritance, particularly, is a great Mk pitfall I expect this method to transparently work with.
I mean if you really need super strong isolation, you can always create a copy of the library object; metaprogramming, dynamic classes, etc, all make it really easy to even, say, create a duplicate class object with references to the original method implementations. Or decorated ones. Or countless other approaches.
My point isn't that I don't see problems that could be solved by this; my point is that I can't think of any problems that this solves, that wouldn't be better solved by things that don't do any innards-fiddling in what is arguably the most sharply-edged part of python: packaging and imports.
And speaking from experience... if you think patching can fail in subtle edge cases, then I've got some bad news for you re: import hooks.
At the end of the day, people who might use this library are looking for a solution to a particular problem. When documenting things, it's really important to be explicit about the pros and cons of your solution, from the perspective of someone with a particular problem, and not from the perspective of someone who's built a particular solution. If I need to drive a nail, and you're selling wrenches, I don't want to hear about all of the features of your wrenches; I want to know if your wrench can drive my nail, and why I would ever want to choose it instead of a hammer.
I can think of a lot of differently-shaped metaphorical nails that fall under the broad umbrella of "I need to change some upstream code but don't want to maintain a fork". And I can think of a whole lot of python-specific specialty hammers that can accomplish that task. But I still can't think of a signle situation where using import hooks to solve the problem is doing anything other than throwing a wrench into a very delicate gearbox. That is the explanation I would need, if I were in the market for such a solution, to evaluate modshim as a potential approach.
It's much cleaner than monkey patching, and it will more likely detect if an update conflicts with your patching.
I've used it by packaging everything through nix, but that can be cumbersome.
Note: Have not touched in > 13 years, so there's that lol
At the time I was working on a million+-line Ruby codebase at Desk.com. We were in a situation where people were monkeypatching out bugs in dependent libraries that weren't patched upstream yet, and then forgetting about them, and they would eventually end up causing problems that were difficult to run down. So I wrote this tool to basically organize and "vet" the monkeypatches BEFORE they were applied, using a runtime test (at app startup/stack load) to see if it was still necessary to apply, and if not, write a warning to stderr. Otherwise, it would re-apply it (but also notify to stderr). I wanted these patches to be a bit noisy so that they wouldn't be forgotten about and so they would be removed once no longer necessary.
Of course, what I'd NOW do instead is 1) fork the library into my own repo, 2) apply the patch, 3) tell my app to use my fork, 4) have some rigorous process to re-depend back on upstream somehow once things had settled again. That would keep things more easily traceable.
I more or less left Ruby and have been doing Elixir for years now, because I realized that functional/declarative is the way to go for long-term code maintenance (and general ease of testing/debugging, and lower production of bugs per LOC written, etc.).
Regarding the naming, I thought that throwing a bunch of monkeypatches at a codebase was kind of like dropping metal balls in a pachinko machine, in that the outcome would be non-deterministic (we avoid non-determinism at all costs!). For example, there was no way to guarantee the order that they would apply in, in case there were conflicts (which also couldn't be detected at the time of application, only via specific unit testing)... If I was smart (I don't remember if I did this or not), pachinko would intentionally apply the patches in a randomized order, so that latent dependency issues would be floated to the top...
I've written a Jupyter client for the terminal (euporie), for which I've had to employ monkey-patching of various third-party packages to achieve my goals and avoid forking those packages. For example, I've added terminal graphics support & HTML/CSS rendering to prompt-toolkit (a Python TUI library), and I've changed aiohttp to not raise errors on non-200 http responses. These are things the upstream package maintainers do not want to maintain or will not implement, and likewise I do not want to maintain forks of these packages.
So far I've got away with monkey-patching, but recently I implemented a kernel for euporie which runs on the local interpreter (the same interpreter as the application itself). This means that my patches are exposed to the end user in a REPL, resulting in potentially unexpected behaviour for users when using certain 3rd party packages in Python through euporie. Modshim will allow me to keep my patched versions isolated from the end user.
Additionally, I would like to publish some of my patches to prompt_toolkit as a new package extending prompt_toolkit, as I think they would be useful to others building TUI applications. However, the changes required need to be deeply integrated to work, which would mean forking prompt_toolkit (something I'd like to avoid). modshim will make it possible for me to publish just my modifications.
Perhaps it's a somewhat niche use-case, and modshim is not something most Python users would ever need to use. I just thought it was something novel enough to be of interest to other HN users.
> messing around with import internals in python is extremely tricky to get right
This is true! modshim has been the most complicated thing I've written by some way!
> My point isn't that I don't see problems that could be solved by this; my point is that I can't think of any problems that this solves, that wouldn't be better solved by things that don't do any innards-fiddling in what is arguably the most sharply-edged part of python: packaging and imports.
All these examples have the dependency order wrong, and you're right on those - it's simpler to wrap them somehow. But this is doing something different, that is either much harder or outright impossible with those methods: Tweaking something internal to the module while leaving its interface alone. This is shown in both their examples where they modify the TextWrapper object but then use it through the library's wrap() function, and modify the Session object but then just use the standard get() interface to requests.
But regardless of the transformation methodology: the import hook itself is just a delivery mechanism for the modified code. There's nothing stopping the library from using the same transformation mechanism but accessing it with dynamic programming techniques instead of an import hook. And there's nothing you can't do that way.