# DigitalOcean dynamic DNS update script

Inspired by a shell script I saw on Github, I put together a Python version to update the dynamic IP address for a subdomain I have that uses DigitalOcean's nameservers. I added a check to see if the IP address actually needs to be updated, and not do the update if the IP address hasn't changed. All of the user-configurable variables - API token, domain, subdomain - are stored in a separate .env file alongside the script.

#!/usr/bin/env python3

# Import required modules
import dotenv
import json
import os
import requests

# Load user-configured variables from .env file
token = os.environ.get('DO_API_TOKEN')
domain = os.environ.get('DO_DOMAIN')
subdomain = os.environ.get('DO_SUBDOMAIN')

# Other variables
check_ip_url = 'https://api.ipify.org'
do_api_url = 'https://api.digitalocean.com/v2/domains/'

# Get the current external IP
def get_current_ip():
curr_ip = requests.get(check_ip_url).text.rstrip()
return curr_ip

# Get the current subdomain IP
def get_sub_info():
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
for record in records['domain_records']:
if record['name'] == subdomain:
subdomain_info = {
'ip': record['data'],
'record_id': record['id']
}
return subdomain_info

# Update DNS records if required
def update_dns():
subdomain_record_id = get_sub_info()['record_id']
print('Subdomain DNS record does not need updating.')
return
else:
'Content-Type': 'application/json',
'Authorization': 'Bearer \$token',
}
data = '{"data":"' + current_ip_address + '"}'
if '200' in response:
else:
print('IP address update failed with message: ' + response.text)
return

if __name__ == '__main__':
update_dns()


It works, but isn't the prettiest Python code out there, and I'm sure if could be made a bit more efficient. Are there any stylistic changes I should make to better adhere to best practices? I have run it through a PEP8 checker, and the only thing it brought up was about the line length in a few places, which I'm not super concerned about.

#!/usr/bin/env python3

import os

import dotenv
import requests

token = os.environ['DO_API_TOKEN']
domain = os.environ['DO_DOMAIN']
subdomain = os.environ['DO_SUBDOMAIN']

records_url = f'https://api.digitalocean.com/v2/domains/{domain}/records/'
session = requests.Session()
'Authorization': 'Bearer ' + token
}

def get_current_ip():
return requests.get('https://api.ipify.org').text.rstrip()

def get_sub_info():
records = session.get(records_url).json()
for record in records['domain_records']:
if record['name'] == subdomain:
return record

def update_dns():
sub_info = get_sub_info()
subdomain_record_id = sub_info['id']
print('Subdomain DNS record does not need updating.')
else:
response = session.put(records_url + subdomain_record_id, json={'data': current_ip_address})
if response.ok:
else:
print('IP address update failed with message: ' + response.text)

if __name__ == '__main__':
update_dns()

1. The script will not work if any variables are missing, so use [...] instead of .get(...) to throw an error ASAP if needed.
2. The DO URLs always start with /{domain}/records/ so I included that in the top level constant.
3. A requests.Session makes multiple requests to the same domain faster as it keeps the connection open, and it lets you specify info like headers once.
4. A few times you create a variable and immediately return it. You can just return the expression directly.
5. I felt that the check_ip_url constant didn't add anything, so I inlined it. This is mostly a preference.
6. Most of the comments do not help readers in any way, so I removed them. But if you want to describe what a function does, use a docstring.
7. Calling .json() on a response parses the text as JSON for you.
8. Moving the values of one dict to another dict before finally extracting them just adds another layer, so I just returned record directly.
9. You called get_sub_info() twice which meant two identical requests. To speed things up, I extracted a variable.
10. Again, requests makes JSON easy to use, now with the json= argument. This both converts the dictionary to a JSON string and sets the content type. But even without this, you really should have been using json.dumps rather than string concatenation.
11. response.ok is typically how you check if a request succeeded, or by checking the value of response.status_code. I've never seen anyone using the in operator on a response.
12. There's no reason to return when a function is ending anyway.
• Thanks for the pointers! But what does the 'f' before the URL in records_url do? – campegg Feb 4 at 20:30
• @campegg Google "python f string" – Alex Hall Feb 5 at 4:56
• Got it - I was looking for "f variables" and things like that, but didn't turn anything up. Thanks again! – campegg Feb 5 at 15:07