Fastcore: A library that extends Python with new features
fastpages.fast.ai
fastpages.fast.ai
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.
- https://toolz.readthedocs.io/en/latest/api.html#toolz.functo...
- https://toolz.readthedocs.io/en/latest/api.html#toolz.functo...
It is not equivalent to PrePostMetaInit.
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.
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.
> 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....
> 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.
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.
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.
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...
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.
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.
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.
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.
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.
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.
$ python -m pdb ...
(Pdb) help pp
pp expression
Pretty-print the value of the expression.
(Pdb) pp ...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:
This function is the result of x() being wrapped with partial(x, 1, foo='bar').
Here is the original docstring of x():
....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) 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...
I stopped reading when the author suggests using
class NewParent(ParentClass, metaclass=PrePostInitMeta): def __pre_init__(self, args, *kwargs): super().__init__()
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 :)
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.
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.
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.
Rather, I thought these syntactic layers made things worse rather than better, in terms of clarity, understandability and ease of writing simple, straightforward code.
I cringe a little when someone says a straight forward thing should be replaced by magic middleware.
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.
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.
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.
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.