> Most metaprogramming shouldn't exist in production code.
Go ahead and try. Many of the libraries you will use use metaprogramming of one flavour or another for ergonomic reasons.
Python decorators are metaprogramming. For example putting @cached above a function declaration esentially wraps your function into another that caches it's values without you ever having to see all of these details.
I am not sure if your production code would really gain from writing out that caching logic over and over again for each of your functions that need caching. I am sure however that it would become less readable and harder to reason about.
In Rust #[derive(Serialize, Deserialize)] can be used to automagically give your data-types serialization and deserialization. This is also metaprogramming. It also reduces the code you have to read and write and the mistakes you will make if you roll this on your own. It also makes your code easier to read to all people who understand what it does (so nearly everyone who programs Rust).
Metaprogramming is okay, if it is done in the right places for the right reasons and doesn't obscure the logic of the program.
I often use it for custom decorators in flask e.g. @admin_user or @authenticated_user to quickly wrap the functions for some http routes with the ever-same logic for authentification. Sure you could also do this with a function, but the ergonomics could be worse and you could accidentally place the function call in the wrong place and thus exposing some parts of a route to unauthenticated users. I don't think this makes it harder to reason about my code.
Like all "don't do X" idioms in programming the one about metaprogramming should be taken with a grain of salt. There are cases where metaprogramming is the best (most reliable, futureproof, usable, etc) way of solving a given problem. The warning is true in that you should avoid using it everywhere without reason. But there are places where it makes sense and there it would be a waste not to use it