Carefully exploring Rust as a Python developer
karimjedda.com
karimjedda.com
I am a Python developer. This is pretty much what defines me. Being a general purpose developer with familiarity to one langauge is what helped make me my living until recently. Now, being a devrel I need to be familiar with a BUNCH of languages at least on the syntax level for our SDKs.
The third/fourth language for a Python developer the default suggestions are Go, Rust, C#, Java/Scala etc....it varies but there is a pattern. A low level language that frequently makes it to HN front page or something that your supervisor wants to explore because they say it in HN front page.
Even though we are a Go shop and Rust is hyped as hyped can be, I don't consider either to be as fun as Python. The syntax and logic feels absolutely weird. Python gets you up and running in no time. But these language just doesn't feel write. Too much verbosity, too much diy, too much etiquette. The things that make these language loved to actual "SWE" makes me hate them, as I am not a "SWE", I just love solving problems programmatically even though it inherently makes me a bad programmer but I am a passable Python Developer and I can live with that.
Then after much doubt and much imposter syndrome-ness, I found it, the perfect third/fourth language for a Python developer, Nim. The tag line itself says all, "zero dependency executables". It is closest thing to feel like Python while it keeps everything elegant (in SWE terms, I guess). This language is a godsend if you are making custom tooling.
Give it a shot, if you made it through this rant.
Minor nit: these two sentences seem to contradict each other.
You can use `cargo add` to add dependencies, it's part of the standard set: https://doc.rust-lang.org/cargo/commands/cargo-add.html
> Writing tests
This section is very odd, I don't think I've ever seen tests set up that way (but I might be wrong).
Normally, you have:
- a tests submodule to unit-test internal behaviour (gated with cfg(test))
- a top-level `tests` subdirectory, which contains integration tests leveraging the public API of the crate
The dead_code strictures are also unnatural, but I guess the author just didn't call their functions from `main.rs`, and didn't re-expose them either?
Anyway usually in that case you'd just `allow` the warning at the toplevel.
> Add the following three lines to your Cargo.toml and you're good to go: reqwest = { version = "0.11", features = ["json"] } and tokio = { version = "1", features = ["full"] } and serde_json = "1".
FWIW reqwest has a `blocking` submodule, so you don't have to explicitly care about async to use reqwest (though it'll still be pulled as I think reqwest just implements the sync API on top of the async one).
Since the article doesn't actually cover async it seems unnecessary to bring it up.
Incidentally, Rust's async is very similar to Python's semantically, but thanks to the static typing it's also significantly more reliable and easier to use (because the compiler can tell you when you're not using / awaiting a coroutine, whereas python will blithely let you ignore it, and only unhelpfully tell you that you've got unawaited coroutines somewhere at program termination).
Modules and crates continues to be one of the most confusing aspects of Rust, for me at least.
Minor nit though: Rust doesn't have exceptions.
This makes panics completely unreliable for error reporting.
There has always been a debate what exceptions are for. Python leans heavily towards "exceptions don't have to be exceptional, they can be normal control flow", while Rust is in the other side of the spectrum closer to "if it's something you might want to handle it's not exceptional enough to be an exception". Java is somewhere in between. All of them are valid view points.
A language without reliable exception mechanism is C, where even the most unusual errors are communicated through return codes or global flags that you are expected to check manually
Not really. If it instantly terminates the program without recourse, it's not an exception system: by definition, exceptions come with exception-handling of some sort.
It's just bad form to do so in Rust because it's bad for maintainability to design programs like that.
Rust is opinionated and very much NOT an anything goes language. There is a culture and the culture says do not catch panics. If you feel the need to catch a panic, make it a regular value instead.
The actual difference is that you can set panic=abort as the user of the code (it's a compiler parameter), in which case there is no unwinding and the program aborts immediately.
To me, the lack of possible handling and recovery (based on a compilation flag) is what arguably disqualifies it as an exceptions system.
A bit like saying trains and chairs are functionally the same since they both can have wheels; when there's the crucial difference that chairs don't go choo-choo.
No, these don’t matter. Every langage with exceptions uses them differently, that doesn’t make Python’s no-exceptions and Java’s exceptions.
Regardless of how it’s used, it either is or is not an exceptions system. That’s what “functionally” means.
For instance Go’s panics are an exceptions system, you can always recover() and handle the exception during its unwinding.
> A bit like saying trains and chairs are functionally the same since they both can have wheels
No, it’s like saying an HSR and an ore train are functionally similar, because both are trains even though they’re used differently, in different contexts, and with different performance profiles.
On hardware level exceptions are a synonym for traps.
Java has the checked/unchecked exception divide, where checked exceptions for expected errors that need to be handled specifically, and unchecked exceptions for unexpected errors, matching the use cases of Result and panic respectively in Rust. Unfortunately Java's checked exceptions are cumbersome and don't integrate well with interfaces or generics.
In C# the role of exceptions is not so clearly defined, largely due to the lack of a good alternative error handling mechanism. You have influential developers like Eric Lippert (then part of the C# compiler team) argue against using exceptions for expected errors https://ericlippert.com/2008/09/10/vexing-exceptions . Though I'd go further and would call the use of exceptions to signal IO errors an unfortunate design decision of the language/framework designers. My own experience as web developer is that exceptions will almost always be caught only at the request handler level and turn into an http error.
In C++ the standard library doesn't use exceptions for most errors. And it's common to avoid exceptions entirely (e.g. the google style guide forbids them).
So I'd say the role of unchecked exceptions is not too different from panics in many other languages, though the situation tends to be more messy in Rust due to legacy code and the lack of good alternative error handling mechanisms.
https://doc.rust-lang.org/nomicon/exception-safety.html
Also, there is something similar to try/catch in Rust to catch exceptions, although the docs emphasize that you shouldn’t use for typical error handling. It’s mainly for when Rust is called from the outside (typically a C program). This is because you don’t want a Rust exception to unwind to external code, or all kinds of weird undefined behavior will happen.
The Result<T,E> type is very explicit, and can be easily ignored, composed or "re-thrown" with the "?" suffix without nested try/catch blocks. Return value based error handling is something Go, Elixir and other more functional languages have also adopted.
Panic is there to aleviate the really exceptional circumstances, when the trade-off for possible program termination is worth the much simplified error handling, such as when casting to a smaller integer type in case of overflow, or locking a mutex when it may be in a posioned state (which, funnily enough, can arrive when a panic occurred whilst the mutex was previously locked, i.e. the mutex guard is dropped during a panic).
It would be nice if Rust grew "panic annotations" so that we could determine shallowly and with automated tooling whether functions could panic. It would make it easy to isolate panicky behavior, and in places where it is absolutely necessary to handle, ensure that we do.
> If the function does panic (or the compiler fails to prove that the function cannot panic), the program fails to compile with a linker error that identifies the function name.
The Rust Programming Language is, to my mind, one of the reasons for Rust's popularity. Every language would go far by having a book of that quality freely available and accessible to such a wide audience. It's, truly, a feat.
Rust is easy to learn by studying (in order to understand its unique concepts such as the borrow checker and lifetime annotations), but Rust is hard to learn by experimentation alone (trying 100 different syntax variations to see what compiles).
[1] https://www.youtube.com/watch?v=FSyfZVuD32Y
[2] https://www.youtube.com/c/rhymu8354/videos
[3] https://www.youtube.com/channel/UCmBgC0JN41HjyjAXfkdkp-Q/vid...
The book does a really good job at explaining the thinking behind the abstractions available in the standard library. To me, this was the book that made everything click, so that I did not feel I was fighting against the language.
[0]: https://www.oreilly.com/library/view/programming-rust-2nd/97...
At some point the similarities with Python stop and the similarities with C++ start. Although without the sins of C++ (like decades of backwards compatibility going back to plain C), which greatly reduces the decision fatigue you get as a C++ developer.
Unlike C++, Rust can be learned. I maintain that nobody truly knows C++. There will be always C++ code that cannot be explained by an expert. Everyone learns a dialect of C++ that they feel comfortable with, but the entire space of what's possible within C++ and the implications of each feature at compile-time and run-time became too complex to be learned.
While the entirety of C++ is impossible to learn, it took me SIGNIFICANTLY less effort to get a grasp of C++ to the point where I could do everything I needed to do with it. Sure, we can call that a dialect, but it's enough.
Rust is an ongoing effort, because you need a lot more of the language as a whole to click before you get to the point where the compiler stops slapping you about elementary things.
Now, if I was designing an application where memory safety and security was very important, I'd probably try and step up that rust knowledge. But in my field (data science) that's just not a big concern and I can code up a C++ solution MUCH faster than I ever could in Rust.
Both C++ and Rust have similar Quiz sites:
https://dtolnay.github.io/rust-quiz/
You'll notice immediately that the Rust questions aren't trivial - this language does have some opportunities for confusion and weird ways to write things that surprise you and the quiz explores some things you'd probably never do and shouldn't do and their consequences are not obvious.
But on the other hand you will quickly also realise that while "That's Undefined Behaviour" could be the correct answer in C++ (it's the correct answer for several of the default questions) it's never going to be the right answer in (safe) Rust by definition.
[1]: https://github.com/PyO3/pyo3
[2]: https://github.com/PyO3/maturin
[3]: https://github.com/PyO3/maturin#examples
Python focuses on being simple, interpreted and dynamicaly typed, Rust requires you to specify the exact types of all the things (like C/C++) which allows it to generate really optimal compiled machine code before execution, it has more information to work with. Accessing a struct/"object" field is not a hash table lookup, it's a direct pointer access, as the exact size of things are known at compilation time.
If you're writing a script, a tool, a small game, it's simpler to use Python. If you're writing a database engine, a mission-critical piece of code that has to behave predictably without the possibility of random GC pauses, anything that has realtime constraints such as audio, lower-level languages like C, C++ and Rust are a must.
A lot of higher-level folk seem to enjoy Rust as well, especially in networking/the web, for whom the speed is worth the additional difficulty and complexity.
There's also type safety. Not just in terms of having strict compile-time types, but also in what those types can actually do for you. With enums, for example, you can specify different states, but also which data is available in those states. For example, you might model messages passed between different nodes as an enum, where one message contains the current date, and another message contains the number of active users. This way, the compiler proves that you can't end up in impossible situations: having a "date" message that doesn't have an attached Datetime instance, say.
Finally, the developer tooling is often leagues ahead of Python. It's possible to get by in Python with venvs, pip, maybe a requirements file, and so on. But there are often a lot of edge cases that this setup doesn't cover, so there's a lot of this party tools to cover up sharp edges - pip-tools, Poetry, etc. And even then, you often end up in odd situations, particularly if you're trying to retrofit these features onto an existing system.
In the meantime, cargo Just Works™. As in, cargo itself covers 90% of the cases I run into (managing dependencies, managing multiple projects, running tests, building, etc), and the 10% is usually very easy to add: cross compiling with the `cross` tool, linting with `clippy`, IDE support with `rust-analyzer` etc. Managing a Rust project is just so much easier than managing a Python project.
Different people will probably say different things, but as someone who used to be a big fan of Python, but now tends to use Rust for most side projects, those were the big advantages that did it for me.
After many years of programming, now i don't care much about "best practice", what i care is in the details. In Rust, it's how i take care of memory usage of every variables.
Or, write the most ugliest code that works the most performant as possible.
Great writing!
Rust is hard, and people should realize it's hard.
One of the problems with C is that it was way harder than most people realized (we just accepted crashes, and then rampant security vulnerabilities, as normal), and so few practitioners got sufficiently competent at it.
One of the problems with Python is that you don't have to understand much, to install a bunch of libraries and copy&paste snippets found in Web searches, to call something done.
Rust will help you with analogs of some of the conventions that a very skilled C programmer might use. But if someone only wants to get a well-paying job and be able to call their sprint tasks done, they probably want Python or Go, because Rust will make them cry.
If I Web search for a Rust thing, I want to be finding genuine hardcore people talking. (This was actually one of the biggest benefits of working in Scheme and Racket -- very high signal:noise ratio, because the practitioners were generally only there because they were very serious and/or very curious. I'm moving to Rust, and hope to find similar.)
So you want the language never to be mainstream?
It's incredibly well-designed, but you can't design away the inherent complexity of writing safe, secure, fast code. There's no way to make that easy. It will always take a lot of passion or training to really master it. (Yes, I know that some of us here "had an easy time" and it "only took a few weeks", I'm referring to the average person.)
And you know what, that's okay. There are lots of industries where only the most hardcore of programmers use the professional-grade tools. There's nothing wrong with that.
Everyone else will probably continue to use tools like Java, Python, etc. because for most situations, they can get the job done with less training, which means less spent on developer salaries, which is most often more than server costs anyway.
I think the best we can hope for is that Rust is taught in university, like how other industries teach usage of their own professional-grade tools. But even that won't mean that most programmers use Rust, I think.
e.g?
I'm currently using it for developing rest api using the excellent warp crate and I wish more people use it for this use case as its quite suited for it.
Another tech community I'm in is known for being extremely helpful to newcomers who are interested (e.g., anyone who's interested will typically get an expert answering their questions). That's different from saying the subject matter is easy, and mass-advertising it to people looking for easy.
Are Unsafe Rust and Procedural (as opposed by "By example") Macros easy? No, unsafe Rust is potentially really hard. If you are implementing a new concurrency primitive or something you're going to need at least the expertise you'd want for the same work in C++. However in Rust you know where the line is, "Huh, I need unsafe, I should find a grown-up". And knowing that the problem areas are flagged gives you confidence everywhere else.
But many ordinary programming tasks are pretty easy, compared to Python you will spend a bit more time with the compiler refusing to compile your program because it's wrong, but on the other hand a bit less time with the program failing at runtime for trivial reasons a compiler would have spotted (compare what happens if you typo a variable name in Rust versus Python!)
When that happens, recruiters will instead look for occupational or open source experiences with Rust, which they have done for other mainstream languages for years and arguably it's how they should have done for Rust anyway. So there is no real problem here.
You can be about as sloppy in Rust as you might in Python or Ruby: the language supports dynamic typing ("Any" trait), reference counted objects etc. But that would involve a lot of boilerplate, so your sloppy code would very much stand out as non-idiomatic.
Rust’s entire raison d’etre is to make secure, performant computing available to everyone. It makes no sense to keep it elite; the idea is to improve the entire state of affairs.
Besides, it’s a big language, and has a wide range of practical idioms: I almost never use reference-counting types in my code, but it’s a perfectly legitimate (and sometimes necessary) feature that also makes the language more accessible to newcomers.
Ridiculous to gatekeep something that you don’t even identify as being part of yet.