Hey, it's great that you're getting into Python. It really made me feel a lot more confident about putting my ideas together in code, and ever since I started using it I've literally done nothing but write little games and scripts, it got me always coming up with new ideas because I know I can actually make them now.
Anyways so I really checked out your code and I'm still working out some things I thought you might like to see, which I would have done a little differently. Generally though for your first project this is impressive, looks like it paid off having past experience programming in other languages. I can post the revised code later once I finish, if you like, but as for review I'd say you took advantage of object orientation which is a good sign lol.
I'm not entirely sure why you put snake settings and such in separate classes from the object it's used in, so unless you had the intention to use those for something I'm not seeing, I might have just clumped them in with their corresponding object classes. Pygame also has a built 2D Vector class "pygame.math.Vector2" but it doesn't really hurt to use your own, and more power to you for taking advantage of the magic "dunder" methods.
One thing I was trying to figure out is how you used the "game_active" variable to limit the game speed. It seems to work ok but I think if you instead looked into limiting frame rate using a pygame Clock "pygame.time.Clock" you could achieve more responsive event handling.
The last thing I'd recommend is instead of immediately calling "sys.quit()" upon pressing q, give the snake class a "running" variable and set it to true upon initialization. Then instead of "while True" for your main game loop use "while self.running" like this.
def run_game(self) -> None:
while self.running:
self._check_events()
...
Also you would then change the code for when q is pressed to "self.running = False", and finally change the main loop at the bottom of snake.py to
if __name__ == "__main__":
snake = Snake()
snake.run_game()
pygame.quit()
sys.exit()
Edit:
The reason for handling quits this way is so the game has a chance to properly quit, as opposed to quitting the instant you click q. This also gives you a chance to save any last minute configurations and ensure any file operations have completed before exiting. I realized I hadn't explained my reasoning for this so I wanted to clarify. I also changed the main loop code a bit, though as your project grows and you have more stuff to do upon exiting it could be a better idea to put the quit and cleanup calls in it's own "terminate()" method. Like this:
def terminate():
# Put any last minute cleanup code here
pygame.quit()
sys.exit()
Then just change the main loop to:
if __name__ == "__main__":
snake = Snake()
snake.run_game()
terminate()
End of edit:
All in all this is really good for your first project and I'm glad you found Python cause it's really awesome

. Hope that helps at least a little, thanks.
Edit: Fixed the code sections