First of all, I think it's fair to say that this is well done given that you just started learning. Since it's an exercise to learn dictionaries, I'll start by addressing problems related to the dictionary use and JSON.
Lookup
You decided to use a dictionary mapping dog names to the remaining properties. This is a perfectly sound idea as long as you treat dog names as unique. However, you don't actually use the dictionary as a key-value store, you just see it as a sequence of (key, value)
pairs. The power of a dictionary is its ability to retrieve a value by key efficiently (in O(1)
on average if you have already learned about this notation). Instead of iterating over all key-value pairs:
for dog, dog_info in dogs.items():
if dog == user_search:
... # Do something
You can just access the value directly:
dog_info = dogs[user_search]
Not only this is way more concise, but it's also more efficient: iteration approach is O(n)
.
Containment
Checking user_search in dogs.keys()
is correct and adds almost no performance penalty, but conventionally you'd just check if user_search in dogs
- dict.__contains__
(the method that powers x in collection
checks) does what one would expect it to do.
dict
constructor and key naming
dict(Age = age, Breed = breed, Location = location)
I often see code like this, and it's a matter of style preference - I would rather use a dict literal here:
{"Age": age, "Breed": breed, "Location": location}
However, unless necessary for some external reason (another system compatibility, legacy clients, ...), usually json keys are either camelCase
or snake_case
. Since you're writing python, picking snake_case
sounds like an obvious choice - using capitalized key names is less obvious to me.
Other
Lint & Format
Consider using ruff
or black
to format your code automatically and never think again about this. Ideally, set some formatter and linter as a pre-commit hook to run whenever you commit your code.
Code organization
Since you only started learning the language, I suspect you aren't yet able to use functions. Try to revisit your code in future when you learn about them and split the flow into independent blocks.
Expanding the existing answer, I see an obvious candidate for a helper function:
answer_info = input("Would you like this dogs information? ")
if answer_info == "Yes" or answer_info == "yes":
...
can become
def confirm(message: str) -> bool:
answer = input(message)
return answer.lower() == "yes"
Usage
Currently I can create and retrieve the data, but can't update or delete. What if I made a typo when entering dog's breed? Or a dog's name?
Reference
Here's how I would have implemented this utility. I'll show the code first and list the non-obvious changes afterwards:
from __future__ import annotations
import json
from pathlib import Path
from typing import Final, TypeAlias, TypedDict
STORE_FILE: Final = Path("dogs.json")
class DogInfo(TypedDict):
age: int
breed: str
location: str
DogStore: TypeAlias = dict[str, DogInfo]
def load_store(allow_missing: bool = True) -> DogStore:
try:
with STORE_FILE.open("r") as f:
return json.load(f)
except FileNotFoundError:
if not allow_missing:
raise
return {}
def save_store(store: DogStore) -> None:
with STORE_FILE.open("w") as f:
json.dump(store, f, indent=4)
def print_dog(dog_info: DogInfo) -> None:
age = dog_info["age"]
breed = dog_info["breed"]
location = dog_info["location"]
print(f"\tAge: {age}, \n\tBreed: {breed}, \n\tLocation: {location}")
def input_dog() -> DogInfo:
age = None
while age is None:
try:
age = int(input("Please enter dogs age: "))
break
except ValueError:
print("Wrong number, please try again")
breed = input("Please enter dogs breed: ").title()
location = input("Please enter dogs location: ").title()
return {"age": age, "breed": breed, "location": location}
def confirm(message: str) -> bool:
answer = input(message + " [Y/n] ")
return answer.lower() in {"", "y", "yes"}
def bye() -> None:
print("Thank you and have a nice day!")
def main() -> None:
dogs = load_store()
user_search = input("Please enter the dog you are trying to find: ").title()
if user_search in dogs:
print(f"\n{user_search} is registered in the database!")
if not confirm("Would you like this dogs information?"):
return bye()
print_dog(dogs[user_search])
else:
print(f"\n{user_search} is not registered in the database")
if not confirm("Would you like to register this dog?"):
return bye()
dogs[user_search] = input_dog()
print(f"\n{user_search} is now registered in the database!")
save_store(dogs)
dogs = load_store() # You don't actually need this, why reload the same data?
if not confirm("Would you like to view the information of this dog?"):
return bye()
print_dog(dogs[user_search])
if __name__ == "__main__":
main()
Please don't be scared!
- Functions: I tried to extract as small helpers as possible to reduce code duplication.
if __name__ == "__main__"
guard - see here, this way you'd be able to import functions from this file
- Type hints - I defined a
TypedDict
that declares a shape of every item explicitly. Python doesn't check this, type annotation are intended for consumption by tools like mypy
.
pathlib.Path
- prefer using explicit data structures. Python's standard library is very expressive and provides convenient helpers for a lot of actions. Here, by using Path
we tell the gentle reader that this isn't just some arbitrary string, but a file name.
- Handling
FileNotFoundError
- it's sound (IMO) to create an empty file if it doesn't exist when the program is run.
- Age validation: let's use appropriate data types for our variables. Age is a number - I chose
int
, you may want to use a float
, but a string doesn't sound like a good plan (with strings a dog of age "9" will be older than a dog of age "10", for example). To account for invalid inputs, we retry input until the value is parseable.
confirm
- it's very common for CLI tools to also treat "y" as "yes" - that's our muscle memory. Also it's good to tell your user what they can input. And finally, it's also common to provide a default for confirmation prompts: "yes" for nonsensitive and "no" for sensitive actions. Default value is conventionally indicated by upper case, while other options are in lower case. So the prompt Go ahead? [Y/n]
is a conventional way of asking, such that "y" or "yes" will confirm, simply pressing "enter" will confirm as well, and anything else will abort. Similarly, pressing "enter" will abort if the prompt is Go ahead? [y/N]
.