I dislike some of these. Some are cool, like 'Self', but giant "swiss army knife" libraries feel odd.
> Avoid boilerplate when setting instance attributes
dataclasses[1]
> Type Dispatch
functools.singledispatch works with type annotations[2]
> A more useful __repr__
dataclasses[1]
> A better pathlib.Path
path_object.read_text()[3]. The pickle stuff is interesting, but I don't see the value over just pickle.load(path_object). Monkeypatching Path to add this fees odd and unpythonic.
I agree on the monkeypatching of pathlib. It makes it difficult for a reader to look up what the functions do.
Providing a different module, like `import nPathlib as Pathlib` would be a lot more discoverable.
Do you have example of environment that can do this? I don’t think even pycharm manages.
While I generally agree with your sentiment, dynamic monkey patching and heavy meta programming like this is usually not very friendly for any implementation of auto complete.
Multimethod library is mature, handles multiple dispatch, works with type annotations, and appears to have been stable and relatively complete more than a half decade before fastcore was released.
Using pickle for "Path.save/load" feels like a shortcut to me that is inappropriate for many circumstances, since loading a pickle in Python literally means running code from that pickle. It's basically eval(Path.read()). It's not the kind of trust relationship that I would typically expect from a load/save pair.
> Wait! What's going on here? We just imported pathlib.Path - why are we getting this new functionality?
If you are writing a library and this is part of your expose, think long and hard.
> Path.ls
Highly gimmicky, doesn't support globs. Arguments are "n_max, file_type and file_exts", in that order. n_max seems largely pointless for real programs and like a workaround for a poor REPL for interactive use. The difference between the last two arguments is that the first one also accepts mimetypes (does this call "file" on every file it sees?).
Arguably the signature of a "Path.ls" should rather look like "Path.ls(glob='<star>', <star>, files=True, dirs=True)" and it should probably not return a bunch of strings.
Distinction without difference, since Path is just sugar around a string. Directory listing functions should generally return something that mimics dirent/WIN32_FIND_DATA to avoid double work. For example, filtering for file type can typically be done without an extra stat call per file.
dataclasses is not very extensible or flexible. It's fine for starting with, but it's not at all a replacement for store_attr etc.
At fast.ai we used dataclasses extensively for a couple of years, but it just kept biting us. So eventually we switched to these different approaches, which have worked much better for us.
singledispatch is nice enough too, but multiple dispatch with typedispatch is fat more flexible and powerful.
Just because something is in the stdlib, doesn't mean it's the perfect solution to every problem, and that everyone should try to hack their code to fit into those existing solutions.
The most used python library for these things is attrs, I think it mostly provides support for slotted classes (more memory efficient), "converters" (normalization), validation and serialization
I didn't know about functools.singledispatch/singledispatchmethod, it looks really nice!
For typing hinting, I've been annotating with @overload which is just god awful ugly. And you still need to switch based on instance type for the actual implementation.
Monkeypatching anywhere except unittests is a nono for me. It adds some black magic you need to know before reading the code.
Am I reading the standard pathlib.Path or the monkeypatched one ? If I have a bug with it, it comes from the standard library or the patch introduced silently ?
Remember the zen of Python:
> Explicit is better than implicit
And I would personally prefer a unification library ( https://github.com/mrocklin/unification/ ) over the type dispatch proposed here. It's just more powerful and closer to pattern matching if that's really what you want.
I tried to prove you wrong and run multiple tests that all shown you were right. Then I tried to run them on different python versions, down to Python 2.7, expecting that my knowledge was just dated.
No, I was just wrong.
Don't know why I was so convinced of this.
> Regarding docstrings you can access the underlying function via the __wrapped__ attribute,
This assumes one knows about that. A lot of people never even heard of partial(), and won't know about _ _wrapped_ _. Not to mention I never seen a tool using help() fetching _ _wrapped_ _._ _doc_ _ if it exists.
> and signature forwarding is too brittle and magical for stdlib.
@functools.wraps() does signature forwarding to great success, and is in the stdlib.
Nevertheless, I don't think it would be the right strategy for partial(), because you don't have the exact same signature. But at least having something like:
This function is the result of x() being wrapped with partial(x, 1, foo='bar').
Here is the original docstring of x():
....
> Monkeypatching anywhere except unittests is a nono for me.
What do you think about adding pprint to builtins so that you don't need to import it each time when debugging? (Or, alternatively, so that you don't need to add it to the top of every single file?) It seems kind of wrong, but the alternatives are bad also. I'm not sure why Python didn't do this themselves when they added breakpoint in 3.7.
pprint is a standard library module which isn’t necessarily available everywhere. You can run python without the standard library, but the moment you add it to the default builtins it needs to become part of the language.
Monkeypatching in one application is maybe okay. You really want pprint in builtins, fine, who is it really hurting?
Monkeypatching in a library like fastcore that many other people use is really bad. All sorts of bugs and incompatibilities will happen down the road, as software becomes dependent on your monkeypatch without anyone realizing it.
If you want it in your debugger (or ipython, etc), add an import to your `.pythonrc` or equivalent. Then it's ready and waiting for you without influencing your production environment or other engineer's testing environments.
If you want to use it in all your modules, create a template/snippet so that it's imported at the top ofevery new file you create. Then it's obvious to every new engineer you hire as to what's going on, rather than having non-standard behavior everywhere without any obvious indications.
Rather, I thought these syntactic layers made things worse rather than better, in terms of clarity, understandability and ease of writing simple, straightforward code.
This library reminds me a lot of the code I've seen from junior developers trying to make things "cool" by removing a few characters of typing in exchange for library abstractions that create headaches down the line. I think features like this take years to naturally evolve in a codebase, on an as needed basis. And once that code base has evolved to reach something resembling non idiomatic Python, it can be quite hard to integrate new devs into the team. Such transitions into custom dsls or nifty tools are inevitable for large code bases, but I try to minimize it as much as possible, even when it hurts productivity. Long term maintainability is more important to me.
I know what you are talking about in general, but most of the stuff in this library seems to either add clarity or help prevent errors. The problem of kwargs where you end up with a bunch of confusion on documentation is definitely a real thing.
Multiple dispatch is also a great thing as well. You don’t realize you need it until you try implementing something unification algorithm.
Compose is nice for reducing the mental overhead of reading nested expressions. However, I do wish they took a line from lodash and implemented flow instead (like compose but reversed so the first argument is invoked first, then the output of that is fed into the second, and so on).
Syntactic sugar is not bad if it increases signal to noise when reading code or if it prevents erroneous usage.
Ok but what’s with the pre post meta init thing? Let’s see a junior run a debugger through that! Dropping one line of code costs several stack frames in your debug session.
It's a feature from the standard library. However it's only normally available in dataclass. fastcore simply brings the same functionality to other classes too.
dataclass has __post_init__ as a mechanism for doing what you'd normally do in __init__ besides what dataclasses do in the generated __init__, because the generated __init__ means you can't do it in __init__, and otherwise you'd have to override (and replicate the function of) the generated __init__.
Theres a lot of good stuff here (@delegates, partialler), sure, but then there's things like the extended Pathlib with pickle functionality; @typedispatch is a nice concept, but the multimethod seems to be more complete implementation of the concept that's been around much longer, so I'm not sure if there is a good case for it that I'm missing or it's just NIH.
All sizable codebases eventually grow their own patterns & utils that have similar abstractions. I'd rather spend my time writing business logic than boilerplate.
> I try to minimize it as much as possible, even when it hurts productivity. Long term maintainability is more important to me.
I don't think this needs to be an either/or choice in these situations. You can have both if you make the make the conscious decision to not fight your tools. Yes, sometimes you have to be more verbose in one language than another, but the productivity hit in that case always pales in comparison to the hours, days, and in some cases I've seen, weeks, lost by someone fighting their language/tools.
My favorite example of this was a developer given a 2 week feature implementation that ballooned to two months. They had minimal experience C++, and they didn't like its looping syntax. Rather than accept that frustration and write the code in a syntax they disliked, they instead spent weeks writing a "re-usable library that abstracts away looping semantics".
Not a good example because being discontent with c++ for loops is how we got https://en.cppreference.com/w/cpp/ranges (which imo is a good addition to the standard library)
I think the sweet spot is the approach taken by Kotlin for making DSLs because it further removes ceremonial boilerplate that Java loves to give you. I think Pythons as minimalist as it needs to be... The best I do is rename imports that are long if anything. I also define function pointers if I reuse the same method call throughout a tree which helps to keep things tight yet still readable.
I’ve seen this happen with senior devs too, but who come from a background other than Python.
There’s a lot of value in deeply learning the internals of a language so you can write good code in the target language rather than developing some mashup of “features”.
Ironically the article begins with be premise of deeply learning python but never touches on the advanced features that were learned and applied to develop the library.
Sorry about that. Indeed, I felt that since he was the author of the library and essential to me learning it I felt the least I could do was to put his name on there. I removed his name via https://github.com/fastai/fastpages/pull/408 -- Sorry for the confusion!
Indeed, even though I learned a ton about python, I didn't feel like what I had to say made for an interesting blog article, and was a bit abstract. I was equally excited about the library from and end user's perspective and decided to blog about that instead.
Lot of the features in this library seem to be targeted at making things clearer, not just making your code as compact as possible. It is a lot cleaner to use type dispatch rather than have some switch case in your function to determine behavior. And it is useful to know where the kwargs is being used rather than have it be a black box. Having to store every parameter from __init__ into self is an annoying pattern I find myself almost always following, so it's nice to have a solution to that too.
Generally I prefer to use vanilla python as much as possible, but I think there is value in having libraries like this, which make things clearer and have less boilerplate without much cost. Most of what I see would be pretty easy to replace with regular python if you decided to move away from the library, there aren't lots of wierd layers being added to your code.
This library reminds me of fast.ai itself. It’s cool how you can do so many things with a few lines of code, and I was impressed the first couple times I used it. Over time, I started to run into problems when I wanted to do something beyond the initial demo. Many parameters are undocumented, functions do something automatically that’s often nice but there’s no clear way to undo it, and side effects are scattered about. I would prefer code that was slightly longer, but simpler and more pythonic.
I actually think the “delegates” syntax for kwargs or something like it is worth adding to the standard library. The rest feels like it’s either covered by dataclasses or not something I personally want in python. Except pre_init and post_init instead of calling super().__init__. But that’s up there with if __name__ == “__main__” for being an annoying idiosyncrasy that’s too hard to change.
> Wait! What's going on here? We just imported pathlib.Path - why are we getting this new functionality? Thats because we imported the fastcore.foundation module, which patches this module via the @patch decorator discussed earlier.
This is a recipe for disaster. Too much magic that makes it harder to debug. Especially for junior developers or people less experienced in python.
I've seen fast.ai code and in terms of good practices it violates a bunch for the sake of convention. A good example is the `from module import *`.
Every fast.ai module defines __all__, and is carefully designed to allow "import *" to be used safely. I'm not aware of any other library that goes to this trouble.
Allowing import \* to be used safely in notebooks is one thing. Using it in a library is very different.
import \* is convenient to use in a shell. But it makes code more difficult to read. If code doesn't explicitly say where variables were imported from, it takes extra time to figure out where variables were imported from - especially if you're reading code on GitHub.
I hope that it will not look like a personal attack but, honestly, without looking at fastai, your replies on this comment thread does not let me think that the library is of very good quality.
I see there the typical pattern of the 'expert beginner' developer.
If you read yourself,you are saying that you know well that it is better to rely on a specific platform (Github) or editor to be able to resolve your mess than having self descriptive/sufficient code...
For the module using a library, when you do an 'import *' it can easily create a very big mess. For example, as a dev you see a piece of code and you don't necessarily know where all the symbols come from and so the associated dependencies. And so if you have to move this piece of code to another place you can have a hard time to ensure and fix imports on the other side to be sure that it matches.
I'm sorry if I'm not clear, but it is so wrong on so many points in an obvious way that I don't even know what to start with.
> I see there the typical pattern of the 'expert beginner' developer.
I see the typical pattern of someone being a jerk calling people "junior developers" without first doing their research on the person and finding out (1) how long they have been a developer and (2) the work that they have created.
Debate why the methods are incorrect instead of saying "you look ugly".
The author has a completely different development environment than you that supports this paradigm (https://nbdev.fast.ai/) that you likely you aren't using. If you read the docs of the library references to this is well documented. However, that isn't going to be apparent to someone who engages in drive-by comments without doing the proper research.
And yes, it does look like a personal attack. Stop it with the lazy take and actually try to read before jumping to conclusions.
I agree with the core of greatgib's comment but disagree with framing around the person. The problem is not with the developer. We're all trying to write code to help each other grapple with complexity.
The problem is with the effect that `import *` has on others' experience of trying to orient themselves to a codebase.
-------------
One of the great things about python is that when you see a variable or method name, you can look up and see where it comes from. This genuinely reduces the cognitive cost of reading a python codebase. Throwing that away has a real impact on people.
I'll own that part of my reaction comes from the fact that I've been writing ruby professionally for 4 years now and I miss python namespaces ever so terribly.
As I stated, my goal was not to say something like "you look ugly", and I did not want to pretend that he is a "junior developer".
What I said is that from his comments, and based on my personal experience, I think that we are in a case of an "expert beginner" as described by this blog:
The meaning of that is probably better explained in the blog post than by me here. But if I would try to summarize the idea, it is like some people that think that they are the smartest devs because they create "code generators" and some very complex code/machinery, when in fact the problem is their useless complexity, and that a good dev would do something a lot simpler.
If we look at the "import " case, this guy is pretending that he knows better; that it is a "better/cleaner way" to do python code.
But, saying that, he contradicts the very large experience of a lot of really experienced devs and even the spirit of the Python language.
Wildcard imports (from <module> import ) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.
So, even if it is a little bit arbitrary, when someone declare "extending Python" to improve it but does not understand the basic design ideas behind Python, I have the feeling that I'm facing an "Expert beginner".
As I say in my sibling comment to the above, I agree with the core of what you say about OP's code.
I share your values about how code should be written, so I'd like to give you a conceptual tool that I've found makes me a more powerful advocate for the things I value.
You say this and there is no good reason to doubt you:
> my goal was not to say something like "you look ugly"
But do your words _achieve_ that goal? You can judge the probability that I and others are wrong to say no. You can judge your comfort with that risk. You can judge if using this mental model would make others really listen to you:
Words often lead people to hold ideas in their mind. You want your words to do that. To have influence means that when someone else goes to make relevant a choice, your words are the mixtape that plays in their mind. In order to for brains to do things, they need to anticipate some reward -- a feeling of discovery, relief, vindication, determination, clarity. If you imagine your words playing in someone's head, you can make a guess at what will give them that feeling. Succeed, and folks want to keep reading and keep considering your ideas.
Words have power. And the most powerful words to a person conjures ideas about who that person is -- or who they can grow to be. Following that, words merely about what people did or can do also have power. Following that, are words about nonliving objects.
* Identity
* Action
* Objects
So what do you think plays in the mind of someone who initially likes Fastcore and reads "Some people that think that they are the smartest devs because they create "code generators" and some very complex code/machinery, when in fact the problem is their useless complexity, and that a good dev would do something a lot simpler."?
Pay attention to some of the words there:
> the smartest devs
> a good dev
These are both statements about Identity. "Who is she?" - "Not a good dev."
Imagine that I thought that these things that Fastcore are good ideas. Then to hold these ideas in mind requires me to endure for a little bit of time the thought that I am the opposite of a smart dev or good dev. When I am my wisest self, whatever. They're just you words, who cares about them? But when I am a bit more of a suggestible mood, planting that idea in my mind prompts an aggressive rejection from my ego. Is my ego response your problem? Not unless you choose to care about how much your words influence my thought.
> this guy is pretending that he knows better
To call someone a liar is a powerful way to make an enemy. I can work with someone who thinks I am mistaken or that I've failed. Science and Engineering draw their power from a keen understanding of error and failure.
But someone who will be lightning-quick to judge me a liar? I cannot trust them any more than they can trust me.
So my advice this: Read your words and exercise care over what they say about a person, their intent, and their actions.
Do I always do this? No, not at all. This is a discipline -- and I am many things but I am not a disciplined man.
Does this always succeed? No, but when it fails, it fails in better ways.
Is thinking like this manipulative? Well, it genuinely can be: I am talking about power after all. But thinking like this can also stop you from using the power of your words carelessly. ... or not. The only thing that can stop you from using any power for evil is your own sense of responsibility to clarity, kindness, or truthfulness.
What you describe as "wrong" is how a great many languages work.
After coding nearly every day for over 30 years, across many languages, I've come to a view that computers should do our work for us where possible, and that valuable programmer time should be spent doing the things that computers can't do for them.
Most languages do not require the programmer to list every single symbol that they use at the top of the file, or expect programmers to read that entire list to figure out where a symbol comes from. Pretty much every modern editor, IDE, and live coding environment will do that for you. Asking the programmer to make a manual list for you really isn't reasonable, IMO.
The reason that `import *` is dangerous in Python is that most people don't define __all__, and unfortunately Python doesn't have an easy way to avoid namespace pollution without that. Many in the Python community have now convinced themselves that's a feature.
Most recently created (I'm counting 20 years as recent) major languages do require or suggest that you specify the namespace a symbol is from if it isn't local. Off the top of my head, Javascript/Typescript, Python, Go, Java, I think it's good practice in Ruby but I'm not positive.
I think C is the only major language I can think of off the top of my head that doesn't suggest namespacing symbols.
> Asking the programmer to make a manual list for you really isn't reasonable, IMO.
I prefer the clarity, it's worth my time. In the same way that most of the modern tooling will handle resolving which symbols are from which packages, most tooling will also handle adding imports for you if you give it a namespace. I just have to carry the mental load of remembering which package has the symbol I need. It's not an issue most of the time because I usually either know the library well enough that I know the symbol and thus the package, or I don't and I have the docs up on a screen.
There are insidious ways both that and the import all resolution can fail. One is if the symbol exists in more than one package. Then all of a sudden the ordering of your imports matters, which I despise. Another way the __all__ imports can fail in weird ways is if you upgrade a package and it stops exporting a symbol you use. Namespaced symbols will start throwing errors that the symbols doesn't exist in the package. __all__ imports mean you now have to figure out where that symbol even came from originally. You can check what got upgraded and go backwards from there, but it's going to confuse junior devs and your DevOps people.
There might also be performance implications. I don't know if Python has the ability to avoid parsing unused parts of modules or not. It's probably generally negligible unless you're writing small CLI tools.
I just looked at the fastai library on the GitHub app on my phone, and tapping on symbols doesn't show where they come from.
By the way, I love your courses, and I appreciate that you've contributed a ton to the community :) I just think the import \* thing could use changing. It's something that's helpful for experimenting, but unhelpful for library users. I've played around with fastai2 a bunch, and I think showing imports would've helped me understand the codebase a lot faster. It's also a pretty simple change that could probably be automated.
Not having _all_ is just part of the problem. With "import *" you also unknowingly load and potentially overwrite symbols. If you specify each symbol you use, it's very simple for even the most basic linter to figure you are doing something wrong.
Even beyond that, if you've imported `from foo import bar` and then `from something import *`, it might work just fine. Then you upgrade the `something` module, and it happens to now include an item named `bar`, and you're suddenly breaking your existing usage without ever having changed your code.
Star imports are convenient, but dangerous in so many subtle ways. There's a very good reason that every major Python linter will call this out as bad behavior.
Some of this looks excellent. E.g. I occasionally add features like the kwargs delegate in my own code, and there are plenty of places in scipy where that would be an improvement.
Other bits of it aren't drastically better than the standard library. Their typeddispatch is undeniably useful, but functools.singledispatch covers most use cases, and it'd be a shame to pull in all of fastcore for a single feature that's probably covered by another library (e.g. the multipledispatch library).
Some of this is horrifying. There's nothing wrong with the PrePostInitMeta per se....but I guarantee that's going to be a footgun down the line, and IMO if somebody isn't able to comfortably write that metaclass themselves they probably shouldn't be using it.
Agreed (though it should still work with tooling like Sphinx and would definitely work in a REPL, notebook, or debugger).
That touches on a slightly bigger issue too. IDEs only approximate the language and usually fail miserably if you, e.g., do anything nontrivial with PEP-263. They'll whine when new language features like async or assignment expressions come out, they have the damnedest time inferring the type of inner functions, and so on. An IDE typically offers support for a (hopefully useful) subset of your favorite language.
Dataclasses have __post_init__, but they don't have a __pre_init__, do they? From what I can tell, PrePostInitMeta adds a completely new __dunder__ method with pre_init.
Agreed (mostly...it looks more powerful than dataclasses afaict). It looks like it could have a ton of use cases.
My concern is that it seems easy to get started, but metaclass hackery is hard to get right if you do much of it. Maybe that fear is unfounded; have you found that people tend to use it responsibly?
As a small side-note, on the surface I'm not seeing any reason it couldn't be implemented with something more composable like __init_subclass__, and that might be a nice touch.
Fastai/Fastcore are fascinating in their emphasis on interactive computing in notebooks. I think that starting from the assumption that users will be
(a) primarily using the abstractions in this library interactively
(b) to do analysis, or experiment with many different implementations of similar functions
(c) in long-lived sessions where re-running to the same point may be expensive in time or compute, or impossible due dependence on cell execution order etc.
makes their design choices make more sense to me.
Lots of the functionality identified here sort of nice if you're in a traditional environment, but imperative if you're in (or writing for users in) a notebook environment. For example, the emphasis on preserving propagating signature information in function objects themselves makes sense if you're relying on `inspect` to provide IDE-like autocompletion. The emphasis on easy monkey-patching and psuedo-generics makes sense if you're interactively working with classes in modules you don't control, or can't cleanly reimport.
Not only that, fastai/fastcore themselves are written in Jupyter notebooks via their literate programming environment nbdev [1]!
I don't know that there's a point here beyond a bit of begrudging admiration for how far they've pushed the notebook platform. It makes for a library (and a Python) that looks and feels just a few degrees off-axis from much of the rest of the ecosystem...
Nice work! It's often hard to communicate complexity and justify the abstractions, but nevertheless it's a cool demonstration of what's possible in Python.
It's worth thinking about simplifying code and DSLs, as long as you don't go too crazy.
My personal concern would be the lack of typing annotations (IMO very important to have in a 'standard library'), and even if they were present I doubt something like `store_attr()` would be typeable without a mypy plugin.
Personally, I sometimes trade boilerplate and tolerate code duplication for the sake of the code being transparent to static analysis.
Longtime pythonista and co-organizer of PyATL here. I appreciate the goals of the library and understand the points being made in the code. There are certain things that python has acquired over time that are not as nice as they could be. Or maybe we can say that momentum to improve them reduces over time.
One of these things is the __init__ method. It is not named in a clear manner and can be verbose. The alternative proposed with store_attr is close, but still falls to be descriptive. Ideally, I'd just adopt the keyword "constructor" or "initialize" and have it be synthathic sugar for __init__. I do like the fact that it provides a shorter way to initialize things. That could be adopted with no drawbacks as long as we keep named parameters.
I congratulate and welcome such ideas. Ill invite the library author(s) to join the python core team and discuss the ideas with them. It can take time to get new things into a project like python, but we cant grow a language by doing the same thing over and over. Languages need to evolve (to a certain extent) over time.
The challenge here is that fastcore is a deliberate attempt to study the design axis of "what would happen if we focused on Python's dynamic features, and gave up on static analysis entirely".
Using it effectively really requires a live coding environment, such as Jupyter Notebooks (which we do all our coding in, using nbdev). The majority of Python programmers are not currently using such an environment.
In addition, some of the design choices are literally working around Python design choices which we disagree with, such as the recent change in Python where StopIteration is converted to a RuntimeException, which broke all of our composition of generators.
There are some things, however, which might fit into Python, such as:
- Our ProcessPoolExecutor takes `0` as max_workers, to mean "run in serial", which we find very helpful for debugging
- Something like our `bind` (but less hacky) would be nice
- Multiple dispatch would be great (although our version is customized for data processing - a generic python one would be a little different I think)
- Would be nice if `partial`'s repr showed the original docstring
- Our `delegates` decorator might be worth stealing
Appreciate the response. Would it be OK if I reach out through email? Id like to extend a friendly professional relationship between fast.ai and PyATL.
Too clever, too much magic. I remember I used to create such things in my projects in my earlier days as a dev and was quite proud of how compact things were and how much I understand my way around the language that I was able to come up with it. But at the end of the day, these things have a much higher price than it seems at first. Sparing a few lines here and there is not worth confusing your code's readers or yourself in a year. Standard established "pythonic" solutions are preferable.
Some are nice functions, I guess all of us have our collection of utils that we carry around projects and it's certainly nice to have some of them collected in an installable library. But I'd prefer to have them separately in small, separately installable modules, similar to "more_itertools".
delegates: Maybe it's useful for auto-completion in notebooks.
store_attr(): Too confusing.
Avoiding subclassing boilerplate: don't, it's too confusing.
Type dispatch: perhaps, but I think isinstance is okay too.
A better version of functools.partial: Ok, docstring is kept. Could be even changed in the standard library.
Composition of functions: Okay, could be an addition to functools in the standard library.
A more useful __repr__: Good.
Monkey Patching With A Decorator: Please don't.
A better pathlib.Path: I don't think loading pickle should be a method of Path objects. I already dislike pathlib putting read_text as a method of Path. Why not Path.read_jpeg_image()? Path.save_mp4_video() etc. Also don't monkey patch Path please.
Self: too confusing, little upside to it. It makes things look like the function is being called, while actually only a lambda is produced. Too much magic.
in_notebook(), in_colab(), in_ipython: Useful.
The L list: the numpy style indexing is nice, but you may as well create a numpy array.
What does typed dispatch have to do with virtual functions in C++? This type of dispatch happens at compile-time in C++ and has nothing to do with "bloat" or even virtual functions.
I get the feeling that you don't know what virtual functions in C++ are.
I'd suggest you stop comparing typed dispatch to virtual functions in C++, because they have nothing in common. If you want to draw parallels, make sure to use a suitable analog.
You don't directly use fastcore for most things, but fastai underneath uses fastcore. If you want to extend fastai for custom and advanced tasks, fastcore is actually very helpful to use.
Most don't appreciate how critical it is for an ML researcher to be able to play. The best research in fields like that require a tight feedback loop, since we lack an intuition or logical framework from which to reason about these systems. Instead we have to throw shit at the wall, inspect what sticks, and repeat 1000x. The vast majority of evolutionary leaps that I've seen in ML and fields like it have been from "stupid" stuff no classic researcher in their right mind would try. Instead it arises out of a sense of curiosity and playing around. "I wonder what would happen if we used only attention layers? Wouldn't that be silly?" ...
Tooling is a huge part of that. The cost of iterating needs to be low. If you have to rebuild your entire training loop to try a stupid idea, you're not going to try that idea. That's what makes ecosystems like fast.ai so valuable. The whole thing is designed for fast, cheap, highly malleable iteration.
The design decisions required to achieve that have resulted in a library with its fair share of sharp corners. It's a bit of a holographic codebase, where functionality is splintered all across the surface of it. The focus on Notebooks and code jamming leave engineering traditionalists wanting. Etc.
Don't get me wrong. When I write write most other code, I put my engineering glasses on. I want code to be clear and transparent, maintainable, idiomatic, etc. It's just that in ML play is more critical.
So it's important to look at fastai's codebase through that lens. I disagree with commenters here criticizing the codebase as being akin to what an inexperienced engineer would write. Instead it's clear to me that the fastai developers have made numerous very conscious decisions to break convention in support of their goals. They've done that well. And I think it's all in support of their "fast" ideal.
Honestly if I were to lob real criticism at fastai, it would be to say that it needs more documentation. I know, that's a weird criticism given their use of literate programming and the courses available. But ... for example, I have such a hard time with the DataBlock API. I didn't come out of the courses with a good understand of it and the docs are IMO too lacking to sufficiently teach an intuition about it. I had a model where the "y" variable is generated by the model itself. Was never able to figure out the idiomatic way to express that in fastai. Had to fight the DataBlock API and the training loop system tooth and nail to fit it in. I'm sure there's a way to do it. It just wasn't apparent to me.
Of course, that's a _criticism_, not a complaint. The work of Jeremy and co is just an incredible gift to the world. I don't think it speaks ill to say the docs are lacking. That's true of the vast majority of open source software. Documentation is plain _hard_. And the courses, while they touch on the library a good amount, are primarily focused on teaching ML concepts ... as they should be.
Hell, I bet they are aware of that and their switch to literate programming is probably driven by it. It's probably just early in that cycle and things will continue to get better.
I hope it's clear this comment is more about love of the library than a complaint about what is otherwise an amazing thing.
[Author of the blog here]: Thanks for that. Indeed, there is an opportunity to document the library better. That's why I decided to roll up my sleeves and spend an extended period of time documenting fastcore. It was an amazing educational experience for me. I didn't know that python was this extendable, and I previously wasnt aware of concepts like multiple dispatch. I think there are other contributors currently working on other portions of the library as well. The literate programming environment, which made its serious debut in version 2 (that was just released), definitely encourages this and makes this process really easy.
Quick warning Jeremy Howard, the primary author of fastcore, doesn't give two sts about PEP8 or other Pythonic conventions. He's entirely pragmatic and focused on rapid prototyping and experimentation pipelines. If you like that then fast* libraries will probably appeal. If however you see `from foo import *` and cringe then you're going to have a lot of mental speedbumps to get past with this library.
95 comments
[ 2.8 ms ] story [ 144 ms ] thread> Avoid boilerplate when setting instance attributes
dataclasses[1]
> Type Dispatch
functools.singledispatch works with type annotations[2]
> A more useful __repr__
dataclasses[1]
> A better pathlib.Path
path_object.read_text()[3]. The pickle stuff is interesting, but I don't see the value over just pickle.load(path_object). Monkeypatching Path to add this fees odd and unpythonic.
1. https://docs.python.org/3/library/dataclasses.html
2. https://docs.python.org/3/library/functools.html#functools.s...
3. https://docs.python.org/3/library/pathlib.html#pathlib.Path....
While I generally agree with your sentiment, dynamic monkey patching and heavy meta programming like this is usually not very friendly for any implementation of auto complete.
Only for the first argument. "Note that the dispatch happens on the type of the first argument, create your function accordingly".
Making delegated kwargs transparent is 100% cool though and addresses a huge problem with delegated interfaces. I'd use that all the time.
I agree with you about the swiss army thing. I wish it weren't bundled with all this other stuff.
> Wait! What's going on here? We just imported pathlib.Path - why are we getting this new functionality?
If you are writing a library and this is part of your expose, think long and hard.
> Path.ls
Highly gimmicky, doesn't support globs. Arguments are "n_max, file_type and file_exts", in that order. n_max seems largely pointless for real programs and like a workaround for a poor REPL for interactive use. The difference between the last two arguments is that the first one also accepts mimetypes (does this call "file" on every file it sees?).
Arguably the signature of a "Path.ls" should rather look like "Path.ls(glob='<star>', <star>, files=True, dirs=True)" and it should probably not return a bunch of strings.
dataclasses is not very extensible or flexible. It's fine for starting with, but it's not at all a replacement for store_attr etc.
At fast.ai we used dataclasses extensively for a couple of years, but it just kept biting us. So eventually we switched to these different approaches, which have worked much better for us.
singledispatch is nice enough too, but multiple dispatch with typedispatch is fat more flexible and powerful.
Just because something is in the stdlib, doesn't mean it's the perfect solution to every problem, and that everyone should try to hack their code to fit into those existing solutions.
> At fast.ai we used dataclasses extensively for a couple of years, but it just kept biting us.
What were the issues you had?
The most used python library for these things is attrs, I think it mostly provides support for slotted classes (more memory efficient), "converters" (normalization), validation and serialization
Also it relies on python's type system, which is rather limited.
It's a very large module for what it does, and understanding all the details of how it works in practice is a big challenge.
For typing hinting, I've been annotating with @overload which is just god awful ugly. And you still need to switch based on instance type for the actual implementation.
I will try this way instead!
[1] https://www.python.org/dev/peps/pep-0484/#function-method-ov...
I stopped reading when the author suggests using
class NewParent(ParentClass, metaclass=PrePostInitMeta): def __pre_init__(self, args, *kwargs): super().__init__()
Am I reading the standard pathlib.Path or the monkeypatched one ? If I have a bug with it, it comes from the standard library or the patch introduced silently ?
Remember the zen of Python:
> Explicit is better than implicit
And I would personally prefer a unification library ( https://github.com/mrocklin/unification/ ) over the type dispatch proposed here. It's just more powerful and closer to pattern matching if that's really what you want.
But I think what they did with partial should be in the stdlib.
In fact partial should also be rewritten in C et promoted to built in.
Right now it's 10 times slower than lambda, and you have to know about and wish to import functools, yet still loose the docstring.
Shame for such a great feature.
I tried to prove you wrong and run multiple tests that all shown you were right. Then I tried to run them on different python versions, down to Python 2.7, expecting that my knowledge was just dated.
No, I was just wrong.
Don't know why I was so convinced of this.
> Regarding docstrings you can access the underlying function via the __wrapped__ attribute,
This assumes one knows about that. A lot of people never even heard of partial(), and won't know about _ _wrapped_ _. Not to mention I never seen a tool using help() fetching _ _wrapped_ _._ _doc_ _ if it exists.
> and signature forwarding is too brittle and magical for stdlib.
@functools.wraps() does signature forwarding to great success, and is in the stdlib.
Nevertheless, I don't think it would be the right strategy for partial(), because you don't have the exact same signature. But at least having something like:
What do you think about adding pprint to builtins so that you don't need to import it each time when debugging? (Or, alternatively, so that you don't need to add it to the top of every single file?) It seems kind of wrong, but the alternatives are bad also. I'm not sure why Python didn't do this themselves when they added breakpoint in 3.7.
Monkeypatching in a library like fastcore that many other people use is really bad. All sorts of bugs and incompatibilities will happen down the road, as software becomes dependent on your monkeypatch without anyone realizing it.
If you want to use it in all your modules, create a template/snippet so that it's imported at the top ofevery new file you create. Then it's obvious to every new engineer you hire as to what's going on, rather than having non-standard behavior everywhere without any obvious indications.
Rather, I thought these syntactic layers made things worse rather than better, in terms of clarity, understandability and ease of writing simple, straightforward code.
Multiple dispatch is also a great thing as well. You don’t realize you need it until you try implementing something unification algorithm.
Compose is nice for reducing the mental overhead of reading nested expressions. However, I do wish they took a line from lodash and implemented flow instead (like compose but reversed so the first argument is invoked first, then the output of that is fed into the second, and so on).
Syntactic sugar is not bad if it increases signal to noise when reading code or if it prevents erroneous usage.
It is not equivalent to PrePostMetaInit.
- https://toolz.readthedocs.io/en/latest/api.html#toolz.functo...
- https://toolz.readthedocs.io/en/latest/api.html#toolz.functo...
I don't think this needs to be an either/or choice in these situations. You can have both if you make the make the conscious decision to not fight your tools. Yes, sometimes you have to be more verbose in one language than another, but the productivity hit in that case always pales in comparison to the hours, days, and in some cases I've seen, weeks, lost by someone fighting their language/tools.
My favorite example of this was a developer given a 2 week feature implementation that ballooned to two months. They had minimal experience C++, and they didn't like its looping syntax. Rather than accept that frustration and write the code in a syntax they disliked, they instead spent weeks writing a "re-usable library that abstracts away looping semantics".
But yea, not while you're trying to ship.
I wonder what the vision of Python's steering committee is.
There’s a lot of value in deeply learning the internals of a language so you can write good code in the target language rather than developing some mashup of “features”.
Ironically the article begins with be premise of deeply learning python but never touches on the advanced features that were learned and applied to develop the library.
Sorry for the confusion. :)
Indeed, even though I learned a ton about python, I didn't feel like what I had to say made for an interesting blog article, and was a bit abstract. I was equally excited about the library from and end user's perspective and decided to blog about that instead.
Generally I prefer to use vanilla python as much as possible, but I think there is value in having libraries like this, which make things clearer and have less boilerplate without much cost. Most of what I see would be pretty easy to replace with regular python if you decided to move away from the library, there aren't lots of wierd layers being added to your code.
There are some good ideas in there though, like @dispatch which have potential to improve type checking.
This is a recipe for disaster. Too much magic that makes it harder to debug. Especially for junior developers or people less experienced in python.
I've seen fast.ai code and in terms of good practices it violates a bunch for the sake of convention. A good example is the `from module import *`.
Every fast.ai module defines __all__, and is carefully designed to allow "import *" to be used safely. I'm not aware of any other library that goes to this trouble.
import \* is convenient to use in a shell. But it makes code more difficult to read. If code doesn't explicitly say where variables were imported from, it takes extra time to figure out where variables were imported from - especially if you're reading code on GitHub.
Reading the import line to find where a symbol comes from is really clunky.
I see there the typical pattern of the 'expert beginner' developer.
If you read yourself,you are saying that you know well that it is better to rely on a specific platform (Github) or editor to be able to resolve your mess than having self descriptive/sufficient code...
For the module using a library, when you do an 'import *' it can easily create a very big mess. For example, as a dev you see a piece of code and you don't necessarily know where all the symbols come from and so the associated dependencies. And so if you have to move this piece of code to another place you can have a hard time to ensure and fix imports on the other side to be sure that it matches.
I'm sorry if I'm not clear, but it is so wrong on so many points in an obvious way that I don't even know what to start with.
I see the typical pattern of someone being a jerk calling people "junior developers" without first doing their research on the person and finding out (1) how long they have been a developer and (2) the work that they have created.
Debate why the methods are incorrect instead of saying "you look ugly".
The author has a completely different development environment than you that supports this paradigm (https://nbdev.fast.ai/) that you likely you aren't using. If you read the docs of the library references to this is well documented. However, that isn't going to be apparent to someone who engages in drive-by comments without doing the proper research.
And yes, it does look like a personal attack. Stop it with the lazy take and actually try to read before jumping to conclusions.
The problem is with the effect that `import *` has on others' experience of trying to orient themselves to a codebase.
-------------
One of the great things about python is that when you see a variable or method name, you can look up and see where it comes from. This genuinely reduces the cognitive cost of reading a python codebase. Throwing that away has a real impact on people.
I'll own that part of my reaction comes from the fact that I've been writing ruby professionally for 4 years now and I miss python namespaces ever so terribly.
What I said is that from his comments, and based on my personal experience, I think that we are in a case of an "expert beginner" as described by this blog:
https://daedtech.com/how-developers-stop-learning-rise-of-th...
The meaning of that is probably better explained in the blog post than by me here. But if I would try to summarize the idea, it is like some people that think that they are the smartest devs because they create "code generators" and some very complex code/machinery, when in fact the problem is their useless complexity, and that a good dev would do something a lot simpler.
If we look at the "import " case, this guy is pretending that he knows better; that it is a "better/cleaner way" to do python code. But, saying that, he contradicts the very large experience of a lot of really experienced devs and even the spirit of the Python language.
https://www.python.org/dev/peps/pep-0008/
<<
Wildcard imports (from <module> import ) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.
>>
https://www.python.org/dev/peps/pep-0020/ (Zen of Python)
<<
Explicit is better than implicit.
Simple is better than complex.
...
In the face of ambiguity, refuse the temptation to guess.
>>
Probably not exhaustive, but a blog post attempting to explain the kind of issues to be expected with "import *":
https://medium.com/@s16h/importing-star-in-python-88fe9e8bd4...
So, even if it is a little bit arbitrary, when someone declare "extending Python" to improve it but does not understand the basic design ideas behind Python, I have the feeling that I'm facing an "Expert beginner".
You say this and there is no good reason to doubt you:
> my goal was not to say something like "you look ugly"
But do your words _achieve_ that goal? You can judge the probability that I and others are wrong to say no. You can judge your comfort with that risk. You can judge if using this mental model would make others really listen to you:
-----------------------------------------------------------------------------------------
Words often lead people to hold ideas in their mind. You want your words to do that. To have influence means that when someone else goes to make relevant a choice, your words are the mixtape that plays in their mind. In order to for brains to do things, they need to anticipate some reward -- a feeling of discovery, relief, vindication, determination, clarity. If you imagine your words playing in someone's head, you can make a guess at what will give them that feeling. Succeed, and folks want to keep reading and keep considering your ideas.
Words have power. And the most powerful words to a person conjures ideas about who that person is -- or who they can grow to be. Following that, words merely about what people did or can do also have power. Following that, are words about nonliving objects.
* Identity
* Action
* Objects
So what do you think plays in the mind of someone who initially likes Fastcore and reads "Some people that think that they are the smartest devs because they create "code generators" and some very complex code/machinery, when in fact the problem is their useless complexity, and that a good dev would do something a lot simpler."?
Pay attention to some of the words there:
> the smartest devs
> a good dev
These are both statements about Identity. "Who is she?" - "Not a good dev."
Imagine that I thought that these things that Fastcore are good ideas. Then to hold these ideas in mind requires me to endure for a little bit of time the thought that I am the opposite of a smart dev or good dev. When I am my wisest self, whatever. They're just you words, who cares about them? But when I am a bit more of a suggestible mood, planting that idea in my mind prompts an aggressive rejection from my ego. Is my ego response your problem? Not unless you choose to care about how much your words influence my thought.
> this guy is pretending that he knows better
To call someone a liar is a powerful way to make an enemy. I can work with someone who thinks I am mistaken or that I've failed. Science and Engineering draw their power from a keen understanding of error and failure.
But someone who will be lightning-quick to judge me a liar? I cannot trust them any more than they can trust me.
So my advice this: Read your words and exercise care over what they say about a person, their intent, and their actions.
------------------------------------------------------
Do I always do this? No, not at all. This is a discipline -- and I am many things but I am not a disciplined man.
Does this always succeed? No, but when it fails, it fails in better ways.
Is thinking like this manipulative? Well, it genuinely can be: I am talking about power after all. But thinking like this can also stop you from using the power of your words carelessly. ... or not. The only thing that can stop you from using any power for evil is your own sense of responsibility to clarity, kindness, or truthfulness.
After coding nearly every day for over 30 years, across many languages, I've come to a view that computers should do our work for us where possible, and that valuable programmer time should be spent doing the things that computers can't do for them.
Most languages do not require the programmer to list every single symbol that they use at the top of the file, or expect programmers to read that entire list to figure out where a symbol comes from. Pretty much every modern editor, IDE, and live coding environment will do that for you. Asking the programmer to make a manual list for you really isn't reasonable, IMO.
The reason that `import *` is dangerous in Python is that most people don't define __all__, and unfortunately Python doesn't have an easy way to avoid namespace pollution without that. Many in the Python community have now convinced themselves that's a feature.
I think C is the only major language I can think of off the top of my head that doesn't suggest namespacing symbols.
> Asking the programmer to make a manual list for you really isn't reasonable, IMO.
I prefer the clarity, it's worth my time. In the same way that most of the modern tooling will handle resolving which symbols are from which packages, most tooling will also handle adding imports for you if you give it a namespace. I just have to carry the mental load of remembering which package has the symbol I need. It's not an issue most of the time because I usually either know the library well enough that I know the symbol and thus the package, or I don't and I have the docs up on a screen.
There are insidious ways both that and the import all resolution can fail. One is if the symbol exists in more than one package. Then all of a sudden the ordering of your imports matters, which I despise. Another way the __all__ imports can fail in weird ways is if you upgrade a package and it stops exporting a symbol you use. Namespaced symbols will start throwing errors that the symbols doesn't exist in the package. __all__ imports mean you now have to figure out where that symbol even came from originally. You can check what got upgraded and go backwards from there, but it's going to confuse junior devs and your DevOps people.
There might also be performance implications. I don't know if Python has the ability to avoid parsing unused parts of modules or not. It's probably generally negligible unless you're writing small CLI tools.
By the way, I love your courses, and I appreciate that you've contributed a ton to the community :) I just think the import \* thing could use changing. It's something that's helpful for experimenting, but unhelpful for library users. I've played around with fastai2 a bunch, and I think showing imports would've helped me understand the codebase a lot faster. It's also a pretty simple change that could probably be automated.
Star imports are convenient, but dangerous in so many subtle ways. There's a very good reason that every major Python linter will call this out as bad behavior.
Other bits of it aren't drastically better than the standard library. Their typeddispatch is undeniably useful, but functools.singledispatch covers most use cases, and it'd be a shame to pull in all of fastcore for a single feature that's probably covered by another library (e.g. the multipledispatch library).
Some of this is horrifying. There's nothing wrong with the PrePostInitMeta per se....but I guarantee that's going to be a footgun down the line, and IMO if somebody isn't able to comfortably write that metaclass themselves they probably shouldn't be using it.
That touches on a slightly bigger issue too. IDEs only approximate the language and usually fail miserably if you, e.g., do anything nontrivial with PEP-263. They'll whine when new language features like async or assignment expressions come out, they have the damnedest time inferring the type of inner functions, and so on. An IDE typically offers support for a (hopefully useful) subset of your favorite language.
My concern is that it seems easy to get started, but metaclass hackery is hard to get right if you do much of it. Maybe that fear is unfounded; have you found that people tend to use it responsibly?
As a small side-note, on the surface I'm not seeing any reason it couldn't be implemented with something more composable like __init_subclass__, and that might be a nice touch.
(a) primarily using the abstractions in this library interactively
(b) to do analysis, or experiment with many different implementations of similar functions
(c) in long-lived sessions where re-running to the same point may be expensive in time or compute, or impossible due dependence on cell execution order etc.
makes their design choices make more sense to me.
Lots of the functionality identified here sort of nice if you're in a traditional environment, but imperative if you're in (or writing for users in) a notebook environment. For example, the emphasis on preserving propagating signature information in function objects themselves makes sense if you're relying on `inspect` to provide IDE-like autocompletion. The emphasis on easy monkey-patching and psuedo-generics makes sense if you're interactively working with classes in modules you don't control, or can't cleanly reimport.
Not only that, fastai/fastcore themselves are written in Jupyter notebooks via their literate programming environment nbdev [1]!
I don't know that there's a point here beyond a bit of begrudging admiration for how far they've pushed the notebook platform. It makes for a library (and a Python) that looks and feels just a few degrees off-axis from much of the rest of the ecosystem...
[1]: https://nbdev.fast.ai
My personal concern would be the lack of typing annotations (IMO very important to have in a 'standard library'), and even if they were present I doubt something like `store_attr()` would be typeable without a mypy plugin.
Personally, I sometimes trade boilerplate and tolerate code duplication for the sake of the code being transparent to static analysis.
One of these things is the __init__ method. It is not named in a clear manner and can be verbose. The alternative proposed with store_attr is close, but still falls to be descriptive. Ideally, I'd just adopt the keyword "constructor" or "initialize" and have it be synthathic sugar for __init__. I do like the fact that it provides a shorter way to initialize things. That could be adopted with no drawbacks as long as we keep named parameters.
I congratulate and welcome such ideas. Ill invite the library author(s) to join the python core team and discuss the ideas with them. It can take time to get new things into a project like python, but we cant grow a language by doing the same thing over and over. Languages need to evolve (to a certain extent) over time.
The challenge here is that fastcore is a deliberate attempt to study the design axis of "what would happen if we focused on Python's dynamic features, and gave up on static analysis entirely".
Using it effectively really requires a live coding environment, such as Jupyter Notebooks (which we do all our coding in, using nbdev). The majority of Python programmers are not currently using such an environment.
In addition, some of the design choices are literally working around Python design choices which we disagree with, such as the recent change in Python where StopIteration is converted to a RuntimeException, which broke all of our composition of generators.
There are some things, however, which might fit into Python, such as:
- Our ProcessPoolExecutor takes `0` as max_workers, to mean "run in serial", which we find very helpful for debugging
- Something like our `bind` (but less hacky) would be nice
- Multiple dispatch would be great (although our version is customized for data processing - a generic python one would be a little different I think)
- Would be nice if `partial`'s repr showed the original docstring
- Our `delegates` decorator might be worth stealing
My email is in my profile :)
I cringe a little when someone says a straight forward thing should be replaced by magic middleware.
Some are nice functions, I guess all of us have our collection of utils that we carry around projects and it's certainly nice to have some of them collected in an installable library. But I'd prefer to have them separately in small, separately installable modules, similar to "more_itertools".
delegates: Maybe it's useful for auto-completion in notebooks.
store_attr(): Too confusing.
Avoiding subclassing boilerplate: don't, it's too confusing. Type dispatch: perhaps, but I think isinstance is okay too.
A better version of functools.partial: Ok, docstring is kept. Could be even changed in the standard library.
Composition of functions: Okay, could be an addition to functools in the standard library.
A more useful __repr__: Good.
Monkey Patching With A Decorator: Please don't.
A better pathlib.Path: I don't think loading pickle should be a method of Path objects. I already dislike pathlib putting read_text as a method of Path. Why not Path.read_jpeg_image()? Path.save_mp4_video() etc. Also don't monkey patch Path please.
Self: too confusing, little upside to it. It makes things look like the function is being called, while actually only a lambda is produced. Too much magic.
in_notebook(), in_colab(), in_ipython: Useful.
The L list: the numpy style indexing is nice, but you may as well create a numpy array.
A lot of the proposed value-add seems like the authors aren't aware the built-in `type()` can dynamically modify class definitions.
Getting rid of `hex(id(self))` in `__repr__` is regressive UX.
@typedispatch seems like both a slow way of using 'isinstance()' and a FOMO C++ virtual function bloat.
What's costlier, the new stack frames plus conditional for a decorator or the `elif isinstance()`s in one function definition?
How does a programmer reference the correct function definition by name if they're using @typedispatch instead of `isinstance()`?
I'd suggest you stop comparing typed dispatch to virtual functions in C++, because they have nothing in common. If you want to draw parallels, make sure to use a suitable analog.
You don't directly use fastcore for most things, but fastai underneath uses fastcore. If you want to extend fastai for custom and advanced tasks, fastcore is actually very helpful to use.
Most don't appreciate how critical it is for an ML researcher to be able to play. The best research in fields like that require a tight feedback loop, since we lack an intuition or logical framework from which to reason about these systems. Instead we have to throw shit at the wall, inspect what sticks, and repeat 1000x. The vast majority of evolutionary leaps that I've seen in ML and fields like it have been from "stupid" stuff no classic researcher in their right mind would try. Instead it arises out of a sense of curiosity and playing around. "I wonder what would happen if we used only attention layers? Wouldn't that be silly?" ...
Tooling is a huge part of that. The cost of iterating needs to be low. If you have to rebuild your entire training loop to try a stupid idea, you're not going to try that idea. That's what makes ecosystems like fast.ai so valuable. The whole thing is designed for fast, cheap, highly malleable iteration.
The design decisions required to achieve that have resulted in a library with its fair share of sharp corners. It's a bit of a holographic codebase, where functionality is splintered all across the surface of it. The focus on Notebooks and code jamming leave engineering traditionalists wanting. Etc.
Don't get me wrong. When I write write most other code, I put my engineering glasses on. I want code to be clear and transparent, maintainable, idiomatic, etc. It's just that in ML play is more critical.
So it's important to look at fastai's codebase through that lens. I disagree with commenters here criticizing the codebase as being akin to what an inexperienced engineer would write. Instead it's clear to me that the fastai developers have made numerous very conscious decisions to break convention in support of their goals. They've done that well. And I think it's all in support of their "fast" ideal.
Honestly if I were to lob real criticism at fastai, it would be to say that it needs more documentation. I know, that's a weird criticism given their use of literate programming and the courses available. But ... for example, I have such a hard time with the DataBlock API. I didn't come out of the courses with a good understand of it and the docs are IMO too lacking to sufficiently teach an intuition about it. I had a model where the "y" variable is generated by the model itself. Was never able to figure out the idiomatic way to express that in fastai. Had to fight the DataBlock API and the training loop system tooth and nail to fit it in. I'm sure there's a way to do it. It just wasn't apparent to me.
Of course, that's a _criticism_, not a complaint. The work of Jeremy and co is just an incredible gift to the world. I don't think it speaks ill to say the docs are lacking. That's true of the vast majority of open source software. Documentation is plain _hard_. And the courses, while they touch on the library a good amount, are primarily focused on teaching ML concepts ... as they should be.
Hell, I bet they are aware of that and their switch to literate programming is probably driven by it. It's probably just early in that cycle and things will continue to get better.
I hope it's clear this comment is more about love of the library than a complaint about what is otherwise an amazing thing.