Categories Tutorials & Tricks

Tutorial #1 – Using the USZIPCODE Python Library to expand a weather app

As I’ve mentioned a few times on Twitter, recently, I’ve started using Mike Kennedy’s (of TalkPython.fm) Python Jumpstart course.  It really is a great course, so check it out.  I tried the book “Automate the Boring Stuff with Python” first, but I find Mike does a really good job of making some of the underlying principles a little more solid.

I’m also something of a tinkerer, so I’ve been taking all the courses and trying to either make a parallel project, or putting my own spin on it, because I find that helps me more than merely copying along.  (Seriously, if you’re new and you’re not already doing this, it’s a good idea.)

So, I got to the fifth app – a weather app – and I immediately knew what I wanted to do…  make it work with National Weather Service instead of Weather Underground (which is what the course used).  But, what I also wanted to do was, rather than merely asking the user for their zipcode – I wanted to ask for a city & state, and then get a zipcode for them!

Surprisingly, these things aren’t as unrelated as they might appear at first blush:

def get_location():
    search = ZipcodeSearchEngine()

    location_query = input("What city are you looking for? \"City, ST OR zip.\"\n")

    if location_query.isnumeric() == True:
        zipcode = search.by_zipcode(location_query)
        city = zipcode.City
        state = zipcode.State
        latitude = zipcode.Latitude
        longitude = zipcode.Longitude
        zipcode = zipcode.Zipcode

    elif location_query.isnumeric() == False:
        if location_query[-3] == "," or location_query[-4] == ",":
            city,state = location_query.split(",")
            state = state.strip()
        else:
            city,state = location_query.split(" ")
        request = search.by_city_and_state(city,state)
        zipcode = request[0].Zipcode  
        latitude = request[0].Latitude
        longitude = request[0].Longitude

    location = Lookup_Location(city, state, zipcode, latitude, longitude)

    return location

Let me assert before I go any further that aside from 1 line, I’m going to try to avoid using any code from the TalkPython Training Course, because I really do think people should use it.

To make this work, you’ll need to pip install the uszipcode library, and then “from uszipcode import ZipcodeSearchEngine“.

Then I’m going to try to break things down in a logical way for people who, like me, are new users, by working my way inward.  (If this is not how it’s usually done, please let me know.)

I’d like to explain why you do Line 2, but I took that directly from the uszipcode docs, so maybe someone smarter than me can explain it.  I’ll just say, copy that for now. (Note that all “search.xyz” below is referencing that ‘search’, so if you rename that, rename the rest accordingly.)

That gets us to this “if” set, which checks to see whether or not the user entered a zip code (numeric) or city,state(not numeric):

if location_query.isnumeric() == True:
elif location_query.isnumeric() == False:
If the user entered a zip code, it’s a pretty straightforward process:
    zipcode = search.by_zipcode(location_query)
    city = zipcode.City
    state = zipcode.State
    latitude = zipcode.Latitude
    longitude = zipcode.Longitude
    zipcode = zipcode.Zipcode

This searches the database for the user-provided zipcode (which returns a dictionary of about 30 data points for that zipcode), and then pulls the relevant ones from it – “City, State, Latitude & Longitude”.

But if the user enters a City & State, it’s a little bit more complex.

Originally, this is what I had there:

    city,state = location_query.split(",")
    state = state.strip()  

This would split Boston, MA into [Boston, MA] and then into [Boston,MA].(Reminder that this comma is b/c of the list, not inherited from the string.)

state = state.strip() is just a safeguard. If the user writes Boston,MA it’s not needed and doesn’t trigger. If it IS needed, it’ll trigger.

However, I found this a bit more robust:

    if location_query[-3] == "," or location_query[-4] == ",":  # ADVANCED PRECAUTION
        city,state = location_query.split(",")
        state = state.strip() 
    else:
        city,state = location_query.split(" ")

The conditional under the “if” is the code from above. This is basically extra caution – if there’s a comma (Phoenix, AZ) it will treat it as above. If there’s no comma, it will just split it on the space. This is to prevent it from not splitting at all.

Note that there is still the possibility that the user will write out the full city and state (e.g. Phoenix, Arizona), which this will not catch. Hopefully, between the subtle cuing of ST instead of State, and people’s laziness when typing, this will solve itself. If you wanted to be really careful, you could obviously use a Regex to do this, or set it up as a set of slices based on the length of state names (thusly: if location_query[-3:-14] == “,)

Which brings us to the last section of that if:
    request = search.by_city_and_state(city,state)  # uses uszipcode to find all info for that city
    zipcode = request[0].Zipcode  # gets a zipcode within the city.
    latitude = request[0].Latitude
    longitude = request[0].Longitude
 

The one important difference between searching by zip code and searching by city and state is that a zip code is a unique identifier, while a city can have multiple zip codes. (Technically one zip code can have multiple cities, but the library so far seems to pick a primary city.) Therefore the request[0] is to ensure that if there’s multiple zip codes for a city, you only pick one. The reason we use 0 is for the cities that have only 1 zip code. Yes, this may mean that you’re not working from the user’s specific zip code, but it’ll at least be within a few miles of them. If they want precision, they should use a zip code.

The last two lines are also pretty simple:

    location = Lookup_Location(city, state, zipcode, latitude, longitude)
    return location

This bundles all 5 pieces of data into a named tuple and returns it. You can use the same variable name in the tuple whether they come from the Zip search or the City search, because only one will be used, so there’s no risk of them overwriting.

Which brings us to our last part:

    NWS_url = "https://forecast.weather.gov/MapClick.php?lat={}&lon={}#".format(location[3],location[4])
    Weather_Underground_url = "https://www.wunderground.com/weather/{}".format(location[2])

In this, location refers to the named tuple from above. The weather underground url uses location[2] (zipcode).
However, National Weather Service uses latitude and longitude. This is why we pulled those into our tuple, and they’re location[3] and [4].

Another oddity about the NWS page:
https://forecast.weather.gov/MapClick.php?lat=NUMBERS&lon=NUMBERS#.WxddZEgvyUk
The part in red is on every lookup I’ve done, but it’s always a different one. My speculation is that it’s possibly related to what time you did the search. I’m not sure. However, you can remove it from your search and it’ll still work fine.

So that’s that. If you want to see how to get the rest of it working, then I, again, recommend checking out the Python Jumpstart by Building 10 Apps course.  Unlike Penn & Teller, I’m only willing to reveal my own tricks.

If you found this useful, or if you have any suggestions for how I can do future ones of these better, please drop me a comment.

And if you’d like to see the finished version of my app, you can check it out on my Github page.

About the author