I'd strongly recommend to go for the first approach, even if you could reject all my arguments against the second.
Your first approach, overriding getters, has many advantages. The first is that it will work without surprise in many languages.
Moreover, in the spirit of the open/closed principle it leaves a lot of possibilities open for the future. For example, since the maximum age is per instance, you could imagine a
class Tortoise that calculates the age based on the size of the individual instead of a fixed number. You could even calculate the maximum age on the spot, based on statistical indicators of the individual's location, in real time.
The second approach, overriding attributes, has many inconveniences. First, it will fail in the many languages that do not support attribute overriding. Some languages will seem to accept it but in reality silently create additional members in a different scope (e.g. Java, C++). This might result in different methods accessing different members sharing the same name, and very nasty bugs. But ok: in python it works.
- you expose the member, which might weaken encapsulation. But ok: in python, it's a risk that has to be accepted.
- The modification of the overridden attribute is a serious risk regarding Liskov Substitution Principle, and more precisely the history constraint, which requires base variables/state to modified via base methods. This might lead to nasty bugs, in any OOP language.
- It assumes one fixed
max_age per species. And if it's not the case, you'll have to calculate and update the maximum age in different place (size, gender, year of birth, ...) even if you'll never use it.
- It's just not as flexible as the first one. It's a consequence of the underlying assumption. Take the
Tortoise example above. To get the realtime feature would require to track a realtime indicator to always use the latest value. This could be very inefficient. But ok, you might never need this.
That's 3 design principles and a couple of practical points that make the first approach a more recommendable habit. If you would go for the second approach, then it would be safer not to override the member variables, but to define their value via the supertype's constructor, or using a supertype setter.