4
\$\begingroup\$

This is a shopping list program made in Python 3 and it will send the list by email. I am not very experienced but I consider this program as a starting point to what I will do next.

import smtplib

from email.message import EmailMessage

from getpass import getpass

name = str(input("Name: "))
email = str(input("Email: "))
password = str(getpass("Password: "))
recipient = str(input("Recipient's email: "))

while True:
    try:
        productsnm = int(input(f"Hi, {name}!\nHow many products do you want to add to the shopping list? " ))
    except ValueError:
        print ("Please input a number.")
    else:
        break

products = []
quantities = []

for x in range(int(productsnm)):

    product = str(input("Input the product name: "))

    while True:
        try:
            quantity = int(input("Input the product quantity: "))
        except ValueError:
            print ("Please input a number.")
        else:
            break

    products.append(product)
    quantities.append(quantity)

print ("\nThese products have been added to the shopping list:")

for x, y in zip(products, quantities):
    print (f'{x} x {y}')

gmail_user = email 
gmail_password = password

msg = EmailMessage()
msg['Subject'] = "Shopping List"
msg['From'] = gmail_user
msg['To'] = [recipient]
message = ""
for i in range(max(len(products), len(quantities))):
    message = message + str(products[i]) + " x " + str(quantities[i]) + "\n"
msg.set_content(message)

try:
    s = smtplib.SMTP_SSL('smtp.gmail.com', 465)
    s.ehlo()
    s.login(gmail_user, gmail_password)
    s.send_message(msg)
    s.quit()

    print ("\nThe email has been sent.")

except:  
    print ("\nAn error occurred.")

print ("\nHave a nice day!")

exit()
\$\endgroup\$
2
\$\begingroup\$

Functions, functions, functions. They make your life a lot easier, because they allow you to give a clear name to things and even add a docstring to it to add even more description of what the function does. It also means that instead of having to read a whole block of code it is usually enough to read the name of the function to know what is happening (if it is a good name, of course, one of the harder problems in computer science).

Now, let's get to defining some functions. Ideally each function is responsible for one thing, and one thing only (the Single Responsibility Principle).

First, a function that asks for user input, with a given message and a type:

def get_user_input(message, type=str):
    """Ask the user for input using `message`.
    Enforce that the input is castable to `type` and cast it.
    """
    while True:
        try:
            return type(input(message))
        except ValueError:
            print (f"Please input a {type}.")

Note that in your case calling str on input is not necessary in Python 3, since it always returns a str. But here we need it to allow accepting arbitrary types.

Next, I would keep your products and quantities as a dictionary of product_name: quantity key-value pairs. This even allows you to aggregate items if I enter an item multiple times:

def add_item(shopping_list):
    """Add an item to the shopping list.
    If the item already exists in the list, add the quantity to it.
    """
    name = get_user_input("Input the product name: ")
    quantity = get_user_input("Input the product quantity: ", int)
    shopping_list[name] += quantity

This would normally lead to a KeyError whenever name is not already in the dictionary, but will work out fine if items is a collections.defaultdict(int).

Printing the shopping list is also easy:

def print_list(shopping_list):
    for name, quantity in shopping_list.items():
        print(name, "x", quantity)

And emailing the list to someone should also be a function:

def email_to(shopping_list, from_email, password, *recipients):
    email = EmailMessage()
    email['Subject'] = "Shopping List"
    email['From'] = from_email
    email['To'] = recipients
    message = "\n".join(f"{name} x {quantity}" for name, quantity in shopping_list.items())
    email.set_content(message)

    try:
        s = smtplib.SMTP_SSL('smtp.gmail.com', 465)
        s.ehlo()
        s.login(from_user, password)
        s.send_message(email)
        s.quit()
        print ("\nThe email has been sent.")
    except Exception as e:  
        print ("\nAn error occurred:", e)

This function could probably be split up further to allow e.g. using a different provider than GMail.

Note that catching all exceptions with a bare except means also e.g. the user pressing Ctrl+C, which you usually don't want to do. Instead at least catch only Exception and be more specific if you can. You should also print to the user which exception occurred, for which you can use the as keyword.

Then your main calling code becomes:

import smtplib
from email.message import EmailMessage
from getpass import getpass
from collections import defaultdict

if __name__ == "__main__":
    name = input("Name: ")
    n = get_user_input(f"Hi, {name}!\nHow many products do you want to add to the shopping list? ", int)
    shopping_list = defaultdict(int)
    for _ in range(n):
        add_item(shopping_list)
    print_list(shopping_list)

    email = input("Email: ")
    password = getpass("Password: ")
    recipient = input("Recipient's email: ")
    email_to(shopping_list, email, password, recipient)

As you can see, almost all functions actually take shopping_list as a first argument. So you could also make this into a class:

class ShoppingList:
    def __init__(self):
        self.items = defaultdict(int)

    def __str__(self):
        return "\n".join(f"{name} x {quantity}" for name, quantity in self.items.items())

    def add_item(self, name, quantity):
        self.items[name] += quantity

    def email_to(self, from_email, password, *recipients):
        ...

if __name__ == "__main__":
    name = input("Name: ")
    n = get_user_input(f"Hi, {name}!\nHow many products do you want to add to the shopping list? ", int)
    shopping_list = ShoppingList()
    for _ in range(n):
        name = get_user_input("Input the product name: ")
        quantity = get_user_input("Input the product quantity: ", int)
        shopping_list.add_item(name, quantity)
    print(shopping_list)

    email = input("Email: ")
    password = getpass("Password: ")
    recipient = input("Recipient's email: ")
    shopping_list.email_to(email, password, recipient)
\$\endgroup\$
  • \$\begingroup\$ So which is better? With or without the class? \$\endgroup\$ – David Andrei Jan 27 at 16:45
  • \$\begingroup\$ @DavidAndrei: I think here the version with the class is better, because it bundles up all things that manipulate the shopping list (which makes it a perfect candidate to be an object). I did not write the answer with just the class, though, because I wanted to have two different knowledge steps up from your code, the first being to organize the code use functions, the second being OOP. \$\endgroup\$ – Graipher Jan 27 at 17:26
  • \$\begingroup\$ When I try to do the class version it throws me this error: KeyError: 'main.ShoppingList' and I think it is because of email.set_content. Can you tell me what should I put in email.set_content please? \$\endgroup\$ – David Andrei Jan 27 at 18:42
  • \$\begingroup\$ @DavidAndrei Just replace shopping_list.items() with self.items.items(). Otherwise it should be exactly the same as the first email_to. \$\endgroup\$ – Graipher Jan 27 at 19:03
  • 1
    \$\begingroup\$ Fixed, I had to make message = str(self) \$\endgroup\$ – David Andrei Jan 28 at 7:15
2
\$\begingroup\$

Welcome to Code Review. While your script works as is, it is a good practice to setup and split parts of code into functions, and place the entry point inside the if __name__ wall.


You have an extra conversion to int for productsnm (which could be better named product_count).


Since each product is associated with its own quantity, say hello to a namedtuple:

from collections import namedtuple
Product = namedtuple('Product', ['name', 'quantity'])
# inside the for-loop:
    products.append(Product(name=product_name, quantity=quantity))

You wont need the string manipulation, or the loop. Just printing out the products list should give you something like

[Product(name='n0', quantity=0), Product(name='n1', quantity=1), Product(name='n2', quantity=32)]

Each element of the list now has name and quantity as its property as well.


You can have multiple functions as follows:

  • gather_products_list which returns a list of N products.
  • generate_email_body to process the products provided to the function and generating the raw email body.
  • send_email which accepts the aforementioned raw body, and sends it via the provider of choice.
  • get_integer which repeatedly asks the user to input an integer value. This function could accept an optional prompt message as well as an error message!
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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