4
\$\begingroup\$

I am a beginner programmer and have recently started learning python. I wrote this code as a little project to understand dictionaries better and ended up having to learn to use JSON. I am self-taught, so I don't really know if my formatting and such is the best way; the code does work. Any help would be appreciated.

import json

with open("dogs.json", "r") as f:
    dogs = json.load(f)

user_search = input("Please enter the dog you are trying to find: ").title()

if user_search in dogs.keys():
    print(f"\n{user_search} is registered in the database!")
    answer_info = input("Would you like this dogs information? ")

    if answer_info == "Yes" or answer_info == "yes":
        for dog, dog_info in dogs.items():
            if dog == user_search:
                age = dog_info['Age']
                breed = dog_info['Breed']
                location = dog_info['Location']

                print(f"\tAge: {age}, \n\tBreed: {breed}, \n\tLocation: {location}")
    else:
        print("Thank you and have a nice day!")
else:
    print(f"\n{user_search} is not registered in the database")
    answer_new = input("Would you like to register this dog? ")

    if answer_new == "Yes" or answer_new == "yes":
        age = input("Please enter dogs age: ")
        breed = input("Please enter dogs breed: ").title()
        location = input("Please enter dogs location: ").title()

        dogs[user_search] = dict(Age = age, Breed = breed, Location = location)
        print(f"\n{user_search} is now registered in the database!")

        with open("dogs.json", "w") as f:
            json.dump(dogs, f, indent=4)

        with open("dogs.json", "r") as f:
            dogs = json.load(f)

        query = input("Would you like to view the information of this dog? ")
        if query == "Yes" or query == "yes":
            for dog, dog_info in dogs.items():
                if dog == user_search:
                    age = dog_info['Age']
                    breed = dog_info['Breed']
                    location = dog_info['Location']

                    print(f"\tAge: {age}, \n\tBreed: {breed}, \n\tLocation: {location}")
        else:
            print("Thank you and have a nice day!")
    else:
        print("Thank you and have a nice day!")
\$\endgroup\$
0

2 Answers 2

6
\$\begingroup\$

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].
\$\endgroup\$
6
\$\begingroup\$

Overview

The code layout is good, you leverage others' code with the import and you used meaningful names for your variables. This is also a good first question, considering your are new to this site (and the Stack Exchange network).

DRY

Since you use the same file name (dogs.json) several times, you should assign it to a variable.

You use this line 3 times:

    print("Thank you and have a nice day!")

I suggest wrapping it inside a function and calling it as needed:

def salutation():
    print("Thank you and have a nice day!")

Having it in a function will allow you to add or subtract from it if your requirements change.

You can eliminate multiple checks of the same variable like this:

if answer_info == "Yes" or answer_info == "yes":

by using the same technique you used for the user_search input; call with either title() or lower().

This line is repeated twice:

            print(f"\tAge: {age}, \n\tBreed: {breed}, \n\tLocation: {location}")

Again, I suggest a function for better scalability.

Input checking

It would be good to check to see if the age of the dog is reasonable input. For example, check to make sure it is numeric and within a reasonable range (dog years!).

Documentation

I recommend adding a docstring at the top of the file to summarize the purpose of the code:

"""
Who let the dogs out!
"""
\$\endgroup\$
2
  • \$\begingroup\$ Thanks a lot, that was really helpful. I haven't thought of using functions to reduce repetitive code, so I will definitely be implementing that. \$\endgroup\$
    – Nav
    Commented Nov 14 at 15:30
  • \$\begingroup\$ @Nav: I am so glad you found something in this answer to be of value to you. Since you are a new user: What should I do when someone answers my question?, especially about vote/accept. \$\endgroup\$
    – toolic
    Commented Nov 14 at 17:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.