I feel jumping right into __init_subclass__ without explaining why metaclasses exist and what problems they typically solve excludes the majority of devs on HN. Thereby limiting any discussion only to the advanced Python developer echo chamber, while discussion across very different devs is usually far more interesting.
Simon keeps his TIL (today I learned) blog as a way to take notes mainly for himself, from what I understand.
I like the way he documents everything in Github issues[0]. I learn a new thing or two with every release of Datasette, because there's so much troubleshooting full of relevant links and solutions. Same with his TIL blog.
I've not seen this before but it's so cool! I've lost track of all the ways I've tried to keep track of things I've learned. Maybe this would be a good way to do it.
One use is for e.g. binding of database models for an ORM.
class FooModel(metaclass=Base):
x = Field(type=int, primary_key=True)
y = Field(type=reference(BarModel))
The Base metaclass will set it up to implement methods like save() by inheriting from parent classes, but it would also be nice for the library to have a list of all model types without the library user having to call a method like FooORM.register_type(FooModel). So the metaclass is being used in these classes to build up a dictionary of models when the class definition is encountered.
The metaclass is basically a class that itself builds classes, which means it can be syntactically convoluted.
However, with __init_subclass__ you can write a thing that looks like a regular class with regular parent methods, but instead just gets a method called each time the interpreter encounters a new subclass, which lets you do things like build up that dictionary for your ORM.
What classes are to instances, metaclasses are to classes.
This is from someone who has only ever read about, never used metaclasses, because they are widely regarded similar to git submodules. If you cannot really assert that you need them, you don't. They solve very specific problems, mostly found in library, not user code. A library can then allow user classes to be modified comprehensively. If you control the classes in the first place (not library code), you probably can do without metaclasses.
> What classes are to instances, metaclasses are to classes.
Given the popularity of this construct for analogies and metaphorical comparisons, it should be noted that this is strictly literal. In Python classes are objects, and the classes whose instances are classes are called “metaclasses” (and they are subclasses of the class “type”.)
Parent and grandparent comments and their siblings provide a good summary of metaclasses. One of the best deep dives I’ve seen is this document [1] which is offline but still available through the Wayback Machine.
GP is right that metaclasses are rarely used and you’ll kind of know when you need them.
Because metaclasses are classes, yes, classes whose instances are metaclasses are also classes whose instances are classes and thus are also metaclasses. But you could distinguish this subset of metaclasses as “metametaclasses” if you wanted to, to distinguish them from more general metaclasses just as metaclasses are distinguished from more general classes.
But AFAIK no one has come up with a distinct application for custom metametaclasses which would make having terminology to discuss them necessary or useful other than for entertainment.
* Fluent Python — takes you through Python’s core language features and libraries, and shows you how to make your code shorter, faster, and more readable at the same time
* Serious Python — deployment, scalability, testing, and more
* Practices of the Python Pro — learn to design professional-level, clean, easily maintainable software at scale, includes examples for software development best practices
Yeah this is great, __init_subclass__ comes along to make dynamic injection of methods and attributes on class creation easier, just as the entire practice is fast becoming fully obsolete because type checkers like pylance and mypy report these attributes and methods as errors.
That's why I resist python types. A lot of the usefulness and interestingness of Python is in its dynamic features which aid in rapidly developing complex systems. Hard to imagine a strictly typed future for Python, when you then have little to trade off for its bad performance and lack of real concurrency. JavaScript has seen success as a typed language because it has being "the exclusive frontend language" going for it, and v8 is high performance. And the dynamic capabilities of JavaScript were never that interesting or powerful.
> Hard to imagine a strictly typed future for Python
On the contrary, the dynamic nature of python can make it seem like inaccessible black-magic where you don't even know where to look in the code to see what's even possible. Stronger types will empower users to understand their ecosystems.
I've spent countless hours in rails/django land trying to figure out what's even possible with an object that the ORM or whatever gives me. The human-flavored docs kinda outline it, but if you're in IDE-land, context-switching to prose isn't productive.
Types help humans develop and debug, but they make library authors jump through acrobatics to express what their magic does. This is a limitation of the type-checker implementation(s) not having a strong syntax or notion to capture the runtime-dynamic polymorphism, but it doesn't mean that the concept of types is a flawed idea or not worth using when appropriate.
I like types, but I have a hard time seeing them in Python. If I was forced to use only typed Python, I'd just use golang and get a ton of performance and concurrency that I could never get in typed Python.
Good point about Django though. Django has so much historical stickiness that types probably look attractive for entrenched corporate projects that are hard to port to high performance natively typed languages.
> Yeah this is great, __init_subclass__ comes along to make dynamic injection of methods and attributes on class creation easier, just as the entire practice is fast becoming fully obsolete because type checkers like pylance and mypy report these attributes and methods as errors.
The Python community and the set of people that treat mypy and pylance as dictators rather than tools to be used where appropriate and turned off where not are...not the same thing. (And the latter very much depends on tools built by the rest of the former that internally are wild and woolly, even if they present a nice cleanly typed interface to the consumer.)
> it's not very easy for us to say, "just turn off the type checker when you use our library" :)
Sure, and that is a problem with exposing certain kinds of dynamism when your customer base is the kind that is inflexible about typing.
OTOH, a lot can still be done with dynamism on the inside and a typed public interface, and also there's a significant part of the community that accepts selective disabling of typechecking as an acceptable when dynamism provides adequate benefits.
Mypy is scoped to have different levels of checking per package. Normally it doesn’t scan third party libraries beyond the signature of public functions. See the follow_imports options.
There are a few ways to deal with that. One is to declare the properties directly in the subclass:
class Inject:
def __init_subclass__(cls):
cls.injected = 42
class Child(Inject):
injected: int
print(Child().injected) # 42
This technique isn't too useful in practice, though it has its place (e.g. when creating ctypes.Structures). But there's a clever way to simplify this. Remember that the subclass is a subclass -- it inherits from Inject:
import typing as t
class Inject:
injected: t.ClassVar[int]
def __init_subclass__(cls):
cls.injected = 42
class Child(Inject):
pass
print(Child().injected) # 42
(ClassVar[int] ensures that type checkers know it's a variable attached to the class, not each instance.)
yes you can do that, but if you want to do say, what dataclasses does, where it collects all the Field objects and creates a typed `__init__` method, typing can't do that without plugins. all the current typecheckers hardcode the specific behavior of "dataclasses" without there being any pep that allows other systems (like ORMs) to take advantage of the same thing without writing mypy plugins or simply not working for other type checkers that don't allow plugins.
im not really sure (highly doubtful, much simpler ideas like being able to make your own Tuple subclass are still not ready for primetime) but this would be a pretty tough pep to allow the general "dataclass" concept to be available to other libraries that want to use the same pattern, but not use dataclasses.
Python is the most popular language on the planet because it’s a dynamic language, not in spite of it. If the type checker makes dynamic features difficult, ditch the type checker, not the dynamic features. I can’t wait for this pendulum to swing back the other way.
> Python is the most popular language on the planet because it’s a dynamic language, not in spite of it.
Disagree. It's popular because it has an uncluttered syntax - "executable pseudocode" - and simple semantics - "explicit is better than implicit", "there should be one obvious way to do it", and all that. Metaclasses were never particularly Pythonic (if anything they felt like a more Ruby-style way of doing things, and the ascent of Python over Ruby is a reflection of that difference).
Was there ever a point people were successfully building "backbone" systems in dynamically typed languages? I thought the pythons and Perl's of the world were always mainly doing scripts/interface applications.
See [1] for a PyData London talk I did on "Python at Massive Scale", about JPMorgan's Athena trading and risk management system, used for FX, Commodities, Credit, Equities, etc trading. At that time, Athena had 4,500 Python developers doing 20,000 commits a week.
Also see one recent HN discussion on "An Oral History of Bank Python" [2].
Thank you this is super interesting having only heard vague hand waving about bank python. I'm still a bit dubious on it if you don't have the manpower budget to keep this kind of custom setup running but it's really interesting to know how it's done by people who do.
Dunno if you ever heard of this thing called Facebook ... but for a long time it was implemented almost entirely in PHP.
The truth is, the argument that you cannot implement complex systems in dynamic languages has always been empirically fraught because there are so many counter examples. The problem is, even if static languages are "better" for such things, dynamic languages are certainly not bad enough at it to prevent them being successfully used for large scale systems.
That's my observation as well. If you write typed Python code (or Typescript) you need to constrain yourself to a small subset of what the dynamic language can do. In some cases it's beneficial but often it can be quite stifling, so not sure I'm a big fan of gradual typing anymore.
Yes, this. This is where type checking becomes potentially harmful: you have to start creating loads of explicit subclasses instead of one dynamic class. And then you're writing slow Java.
I'm at a point in my career where I see "magic" and wince hard.
This is so hard to reason about and fix at scale. If you let other engineers run wild with this and build features with it, the time will eventually come to decom your service and move functionality elsewhere. Having to chase down these rabbits, duplicate magic, and search large code bases without the help of an AST assisted search is slow, painful, and error prone.
I spent several years undoing magic method dispatch in Ruby codebases. Tracing through nearly a thousand endpoints to detect blast radiuses of making schema changes impacted by CRUD ops on lazily dispatched "clever code".
I'm sure there are valid use cases for this, but be exceedingly careful. Boring code is often all you need. It doesn't leave a mess for your maintainers you'll never meet.
Python users tend not to behave this way, but seeing posts like this requires me to urge caution.
Comments are underestimated. I'm not saying they should be used as an excuse to use "magic" esoteric code, but sometimes such solutions are needed and it's much better to document them for future maintainers.
Wasn't the idea behind this to make most code boring, and have only a small part of it exciting? Just like when structured programming was created, or even local variables introduced, etc. (Of course, the exciting part had to be moved into the compiler in those cases.)
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).”
I realize this quote isn't meant to be taken literally, and it's a nice one-liner encapsulation of metaclasses, but it always bothered me. The people who "actually need metaclasses" were, at some point, learning about metaclasses for the first time and were "wondering whether they need them".
This quote ignores the middle ground of users who have a valid use case, but don't yet understand metaclasses. Not great advice for people who are trying to learn metaclasses.
I do sympathize with this sentiment, but there are times when "if you're wondering if you need it, you don't need it" is a very true statement, and Python metaclasses are in that boat. To see why, let me explain in more detail:
There's a "basic" [1] understanding of Python's object model, one which understands that most of Python is actually syntactic sugar for calling certain special methods. For example, the expression a[b] is "really" just calling a.__getitem__(b). Except that's not actually true; the "real" object model involves yet more dispatching to get things to work. Metaclasses allow you to muck with that "yet more dispatching" step.
So when do you need metaclasses? When you need to do that low-level mucking--the kind of mucking most won't know about until actually needed. If all you know about metaclasses is kind of what they are, then you very likely haven't learned enough about them to use them to actually need them. Conversely, if you've learned those details well enough to need to muck with them, then you've also learned enough to the point that you can answer the question as to whether or not you need metaclasses.
[1] I suspect most Python users don't even have this level of understanding, which is why I put it in scare quotes.
I suspect Tim's meaning was that ordinary users don't need to know how metaclasses work in detail. It is enough that people know they exist (i.e. do something at time class is defined). If you need them, you are doing deep enough magic that you can take the time to learn how they work.
Maybe he was also suggesting that people should generally not be using them. In my experience, it is exceedingly rare to need them. In 29 years of writing Python code, I think I've used them once or twice.
I think SQLAlchemy makes good use of metaclasses. It's the only thing I've seen that used them in a way that I thought made sense and was well justified (but I have been out of the Python world for a while).
In another language it would be done with code-gen, which would also be fine.
But SQLAlchemy is library code.
I took the time to understand how to use metaclasses, and concluded that I never would, especially not in code I expected another engineer to understand - I expect 90% of people who write Python haven't heard of metaclasses, and 99% have never written one.
There was a time, early on in my python career, that I tried all the different flavors of magic that python provides. These days, I tend to write code for junior developers. I don’t use any magic at all, even when it could make sense.
Once you start playing with magic, you have far fewer eyes and hands that are interested.
In my opinion it's not about dealing with junior developers or even having the ability to understand it. I've been programming for 25 years and to me it's about cognitive load. I despise tricks like this because they put a much higher strain on the programmer. Software is hard enough, let's not make it harder.
If I can look at a piece of code and it's quickly obvious whether it is correct or not is my favorite type of code...
Metaclasses / __init_subclass__ are generally only used if you try to define different / new behavior for how the body of a class is interpreted. So if you'd want to create e.g. an explicit interface system in Python, you might use them. If you're creating an ORM or some other kind of declarative property system, you might use them (but not necessarily, as descriptors are a thing). I think the only use in the standard library is in ABCs and probably the typing module.
I'm not fond of the example given in the post, but I've very happily used __init_subclass__ in a few places where I previously would have needed a decorator. Eg, for "plugin-like" things where I would have otherwise needed a decorator to keep track, or iterated over subclasses, now can just "register" subclasses without any extra work on the child classes. Things that I normally wouldn't expect to change the behavior of the subclass, but where having a no-effort hook at subclass creation time can be extremely useful.
I used to think so, but cls.__subclasses__() makes it possible to just walk the type tree at some later point. I find the lack of state to be a feature. (Normally with a hook like you're talking about, you'd add the class to some list somewhere, which can get out of sync.)
It's used very sparingly in the wild, for cases where you need a bunch of classes that are set up similarly, and ordinary subclassing still results in too much boilerplate.
For example... you might make a class for each HTML tag in an HTML processor, or each drawing command in an SVG processor. My experience is that it's not hard to debug, because all that you're doing is getting rid of some boilerplate.
Yep. As a current functional-language guy, this merely confirms my suspicion that those crazy functional people were onto something.
As it turns out, since the human mind is the real limitation here, anything that stops runaway complexity ("hiding" functionality behind "magic" is actually worse because you're just moving the complexity "away from your mental model" where it can fester and grow out of sight, even if what you see looks "simpler") results in fewer bugs, and more-debuggable bugs.
There are bugs in this code and I'm glad that I'm not the only one that has done it!
graph = {
key: {
p
for p in inspect.signature(method).parameters.keys()
if p != "self" and not p.startswith("_")
}
for key, method in cls._registry.items()
}
The first parameter to a bound method does not have to be called `self`. It's conventional, but not required. Is there a better way in the inspect module to filter these parameters? This comes up more often with classmethods where the naming convention `cls` is most common but I see `klass` somewhat frequently as well.
In [4]: class Test:
...: def test(self, a, b, c=5):
...: return a + b + c
...:
In [5]: inspect.signature(Test.test).parameters
Out[5]:
mappingproxy({'self': <Parameter "self">,
'a': <Parameter "a">,
'b': <Parameter "b">,
'c': <Parameter "c=5">})
Then, any idea how you would address the GP's original point? How should "self" be detected if it can be called something else?
I wish there was a 'Modern Python' tutorial that would walk me through all the new python stuff like this or the type declarations or the new libs, etc.
If you're not averse to books I'm yet again recommending "Robust Python" by Patrick Viafore and for more esoteric things like metaclasses I would recommend "Expert Python Programming, 3rd Edition" by Michal Jaworski & Tarek Ziade. It's not wholly dedicated to metaclasses, so there are tons more to learn from it as well.
I learned about metaclasses in "Fluent Python". An excellent book, but it predates typing, so that's not covered. Then again, I believe a second edition is on its way.
I'm working on a project right now that uses __init_subclass__ and it works great, but takes some tricks (like having classvars with types set) in order to get mypy to cooperate. But it's way easier to reason about than metaclasses.
I've only once used metaclasses but I think it was pretty nifty. The use case was with Python and Gtk+ (both version 2), if you had your UI in a glade file you could define a corresponding class, and it would automatically create members for all your named widgets. It made it a bit like coding in VB:
class MainWindow(gtk.Window):
__metaclass__ = MetaDescribedUI("MainWindow.ui")
def __init__(self):
# now you can use e.g. self.lvFiles or self.btnOK
pass
Although writing it now I could probably do it differently without metaclasses.
The most salient example I can think of is tracking all subclasses of a class. This is how django tracks `Model` subclasses to create a table for each model, for instance. I wrote a blog post on this topic: https://tybug.github.io/2021/11/06/tracking-all-subclasses-o...
I've also used metaclasses in the past to apply a decorator to certain methods of all subclasses, without needing to specify that decorator in the subclass declaration itself.
Is useful to do code generation/transformation, for example in this utility[1] it uses the information provided on the declaration of the class to generate the methods required by the base class.
The user of the utility write this:
class Book(objects.Object):
title: Optional[str]
And the generated class code is:
class Book(Base):
__slots__ = ('title', )
title: typing.Optional[str]
def __init__(self, title=None):
self.title = title
@classmethod
def from_data(cls, data):
title = data.get('title', None)
return cls(title=title)
def to_data(self):
data = {}
if self.title is not None:
data['title'] = self.title
return data
@classmethod
def from_oracle_object(cls, obj):
return cls(title=obj.title)
def to_oracle_object(self, connection):
obj = connection.gettype('Book').newobject()
obj.title = self.title
return obj
More generically, Metaclasses are primarily useful when using (abusing?) the Python type system to represent other type systems. The most common example of this is ORMs like SQLAlchemy, where the Python type system is used to represent tables in a relational database.
Another example is the builtin "Typing" module where the type system is used to provide a language for representing itself. "Union" itself is a type, and metaclasses allow you to set "__getitem__" on the "Union" type such that Union[Int|Str] is valid and represents a new type. Not that I don't know if metaclasses are actually used to build this system internally, but you could use it for many of the features implemented by Typing.
There are alternative ways to go about doing that because Python is so flexible, but IMO metaclasses (or __init_subclass__ as seen in this post) are the least "magic" of the magic ways to tackle problems like these.
You got me curious so I looked it up. Apparently in Cpython 'Union' is not a class itself, but is a special object of type _SpecialForm, which is created by using _SpecialForm as a decorator on a function 'Union' which returns the actual union type given a set of parameters.
Interestingly _SpecialForm uses a mixin _Final to prevent subclassing. This mixin is made using __init_subclass__
I recently used them to create a framework for handling plugins to an IRC bot.
By creating an IrcMessageHandler metaclass, I can easily subclass it to encapsulate functionality into different classes without having to add boilerplate to load each plugin. There's a Plugins directory with a __init__.py that automatically loads all .py files, and the main bot just has to do `from Plugins import *` to load them all. Each plugin script merely needs a `from ircbot import IrcMessageHandler`, then creates a subclass. So there's a Calc class that adds a "!calc" command, Seen class that adds "!seen", and more. Each of them merely needs to include an "on_message" function that parses IRC messages.
You basically need it when you have some very strong opinions on how objects should be created, and this isn't the same as the way that Python does it by default.
To understand this fully you should try to understand how 'type' is the class of a class (which is also an object in python) and you should read how 'type' creates a new class, which is basically the job of your new metaclass should you create one.
Usually you don't need it. Also it's finicky to get right.
Something like __init_subclass__ has a more specific purpose, which I suppose is good. However it also sounds like the kind of thing you probably shouldn't be doing.
The need isn't common, but there are some real uses. The top answer of this SO thread (I didn't read the rest, so there may be more goodies) has some good discussion of use cases including comparison to non-metaclass approaches to the same problems.
For those curious about what metaclasses actually are, I wrote up an article with some motivating examples about this a little while back. Might be worth a read if you’re interested!
Isn't it ironic that one of the most readable procedural languages today has almost unreadable OOP syntax?
People unironically created interfaces and various overloaded operations using dunder methods. Not only do you need to keep a list of them under your pillow, but you must also keep notes on how to construct the interface for each one.
And they look ugly as hell. In a language that prides itself on readability and user friendlines.
My suggestion is to ditch the OOP syntax completely and make a built-in analogue to a C struct. Take inspiration from dataclasses syntax. OOP in a dynamic glue language is a silly idea anyway. Of course, you would have to bump the version to 4.0.
I recently encountered __init_subclass__ for the first time in Home Assistant's custom integrations. Configuration handlers were registerd by "just declaring the class" like so and I couldn't grok how it was registered by the runtime
class MyCustomOptionsFlow(OptionsFlow, DOMAIN="MYPLUGIN"):...
A bit of digging showed that __init__subclass_ was being used effectively as a registration callback, adding the newly defined class/DOMAIN to the global CustomOptions handler at class definition.
Evidence, if any was needed, that OOP, or at least the Python variant, was never designed for metaprogramming. Give me Clojure macros any day of the week over these ugly contortions.
I love that Python is unique and strange enough that merely seeing "__init_subclass__" in this headline is enough to know exactly what language it is talking about ;)
Guess what? Since Creature is not yet defined, your very smart type-checker can't see the "other" parameter. Happened to me in VS Code.
That is the problem with features bolted on to a language where they perhaps don't belong. Now, I know that typing helps when you have simple, functional code. So, this code will work:
Except, it is not that simple. It's not even intuitive. Typing is a great feature on paper, but in practice, it is just something we don't need in a scripting and glue language.
For all of Python's magic hackery that it lets you inject it feels like there are still some obvious 'holes'.
The one that bites me a lot is: I want to easily be able to write a decorator that accesses variables on a particular _instance_ of a class (such as a cached return value), and I want to take a Lock when I do it.
But since decorators are defined on the class method, not the instance, it has to do a bunch of work when the function runs: look at the instance, figure out if the lock is defined, if not define it (using a global lock?), take the lock, then do whatever internal processing it wants. It feels like decorators should have a step that runs at `__init__` time to do per-instance setup but instead I have to figure it out myself.
from functools import wraps
from threading import Lock
def my_decorator(f):
# Do whatever you want to set up the lock.
lock = Lock()
@wraps(f)
def wrapper(self, *args, **kwargs):
# Do whatever with the lock.
lock.acquire()
# Do whatever you want with the instance.
print(self.greeting)
return f(self, *args, **kwargs)
return wrapper
class MyClass:
def __init__(self):
self.greeting = "Hello World!"
@my_decorator
def my_method(self):
print("And all who inhabit it.")
>>> MyClass().my_method()
Hello World!
And all who inhabit it.
Edit: Wait do you want the decorator, before it ever runs, to add a property self.lock? Are you sure you don't want this to be a class decorator that messes with __init__? Because you can 100% wrap init to do that kind of set up.
The problem is that `lock = Lock()` is shared across all instances of the class, which creates contention if there are many of them. To solve this you need to save a per-instance lock, so yes to "Wait do you want the decorator, before it ever runs, to add a property self.lock".
An example usecase is writing a @cached decorator that behaves well in multithreaded programs and can be used on classes that have many instances, with the caching happening per-instance.
I don't think it's nearly as bad. The magic nonsense is fairly well-constrained inside the OOP system and there is, imo, a certain coherent logic to it.
>>> import this
> ...
> Explicit is better than implicit.
> ...
> There should be one-- and preferably only one --obvious way to do it.
It's one of Python's philosophies (/s).
I agree that this is a more explicit way to handle metaprogramming in Python. It's analogous to method decorators, unlike metaclasses, which has no "metamethod" equivalent (AFAIK).
Python is quickly taking the crown for low-barrier to entry, slow, buggy code supported primarily by stackoverflow copy pastes from a never-ending supply of “data scientists”
Quickly seems like an overstatement; you've always been able to write low-barrier to entry, slow, buggy code. Is there a low-barrier language that doesn't end up with a lot of less-than-ideal code samples?
I support over a million lines of powershell. It's pretty hygienic, for the efforts of a hundred low-skill contributors.
The language has flaws. But the function parameters are an joy and incredibly powerful. The validation and type coercion is excellent. The functions are extremely composable.
Overall, it's the epitome of "pit of success".
The same team that has all this powershell also has about 50k lines of Python. The Python has the stench of swamp farts, so I don't think it's team skill that makes the powershell work so well.
Meh, Python really shines at I/O and gluing together C/C++/Fortran code. It's reasonably fast for most tasks, and if it isn't there are tools like Cython that make it pretty trivial to write Python code that achieves C-like execution speed (because it effectively compiles to C code).
I agree that Python is a great glue language (so are Perl and Python).
However, Python really doesn't shine when compared with other memory safe languages like Go, Rust, and Java.
Ultimately, Python is single-threaded by design. There are some hacks to get around this design contraint (multi-processing), but then you are adding an entire additional Python runtime to your system's memory for every "thread" you want to run. Even with asyncio, Python performs the worst when it comes to I/O.
I love Python, but it's important to understand that due to specific design considerations it is not and will likely never be a performant language.
If you can write code twice as fast then you'll write twice as many bugs.
Having been a C++ and python developer I see just as many footguns in both languages and have spent many many many hours dealing with slow, buggy code in either.
There's a lot of nice Python code out there, mostly written by disciplined and experienced systems people.
But I agree about the hordes of data scientists making copy and paste spaghetti. This is evidenced by companies deploying Jupyter notebooks in production, which essentially witnesses the fact that they've given up on getting data people to write quality production Python, and instead they treat their prototype spaghetti as sandboxed blackboxes that can be tested on inputs and deployed if they perform well enough.
I miss it. On the javascript side there was just jquery and a mountain of vanilla js that everyone could at least understand (and no npm dependency issues, ES6+ weirdness, etc) and on the PHP side, all built-in functions were in the global scope (easy to CTRL+F on a single page at a time when search endpoints were quite slow), there were (virtually) no web frameworks (at least I didn't use any until after 2009), and no package management to worry about unless you were doing something weird and/or wrangling with wordpress. As a tween and young teen I had plenty of small web design clients.. local businesses, photographers, etc., and I could build sites 100% from scratch starting from a photoshop mockup to custom PHP/HTML/CSS/JS, 100% no packages or other people's code. Packages have done a ton for us, for sure, but I truly miss those days and feel like we've lost something we'll never get back.
Back then it was actually quite possible to build an entire website or "app" as we call them now from first principles just banging your head against HTML/CSS/JS and whatever server side language you were using. Then the internet grew up, security issues became more prevalent, packages, other people's code, dependency hell, "futures", "promises", server state and client state management, shadow dom, reactivity, etc etc.