DigitalOcean dynamic DNS update script
#!/usr/bin/env python3

import os

import dotenv
import requests

dotenv.load_dotenv()
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()
session.headers = {
    '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():
    current_ip_address = get_current_ip()
    sub_info = get_sub_info()
    subdomain_ip_address = sub_info['data']
    subdomain_record_id = sub_info['id']
    if current_ip_address == subdomain_ip_address:
        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:
            print('Subdomain IP address updated to ' + current_ip_address)
        else:
            print('IP address update failed with message: ' + response.text)


if __name__ == '__main__':
    update_dns()



The script will not work if any variables are missing, so use [...] instead of .get(...) to throw an error ASAP if needed.
The DO URLs always start with /{domain}/records/ so I included that in the top level constant.
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.
A few times you create a variable and immediately return it. You can just return the expression directly.
I felt that the check_ip_url constant didn't add anything, so I inlined it. This is mostly a preference.
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.
Calling .json() on a response parses the text as JSON for you.
Moving the values of one dict to another dict before finally extracting them just adds another layer, so I just returned record directly.
You called get_sub_info() twice which meant two identical requests. To speed things up, I extracted a variable.
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.
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.
There's no reason to return when a function is ending anyway.