Home › Tools & APIs › Reverse IP Lookup in Python: DNS, RDAP and API Examples
Reverse IP Lookup in Python: DNS, RDAP and API Examples
In Python, a reverse IP lookup is three small jobs: socket.gethostbyaddr for the DNS name, an HTTPS request to an RDAP service for the registered holder, and an API call for a company-level verdict. Validate the address first, set timeouts, and treat “no answer” as a normal result.
Validate the address first
The standard ipaddress module parses IPv4 and IPv6 and tells you whether an address is public. Use it to reject junk and private ranges before spending a network call.
import ipaddress
def clean(value):
try:
ip = ipaddress.ip_address(value.strip())
except ValueError:
return None
if not ip.is_global:
return None
return str(ip)
This drops malformed text, private ranges and loopback addresses. Strip ports before calling it, and do not feed it values from headers a client controls unless your own infrastructure set them.
Reverse DNS with socket
The socket module offers gethostbyaddr, which performs the PTR query described in reverse DNS lookup by IP.
import socket
def ptr(ip):
try:
return socket.gethostbyaddr(ip)[0]
except (socket.herror, socket.gaierror):
return None
Many addresses have no PTR record, so None is common. To check forward confirmation, resolve the returned name with socket.getaddrinfo and test whether your original address appears in the results. Set a default timeout with socket.setdefaulttimeout(3) so unresponsive resolvers cannot stall a batch.
RDAP for the registered holder
RDAP returns structured JSON, defined in RFC 7483. The public bootstrap service at rdap.org redirects a query to the right registry. The fields you read depend on the registry, so use .get and expect variation.
import requests
def rdap(ip):
r = requests.get(f"https://rdap.org/ip/{ip}", timeout=5)
if r.status_code != 200:
return None
d = r.json()
return {
"name": d.get("name"),
"range": (d.get("startAddress"), d.get("endAddress")),
"country": d.get("country"),
}
This gives the block label and range. Interpret it as in reverse IP lookup with WHOIS: a provider name means shared space. Be polite with rate limits, and cache answers.
A company-level call
For the question “is this a business network?”, call a company-level API. BusinessMCP documents a single lookup as GET /api/hub/v1/ip/{ip} on its IP to company API page, with verdicts company, non_business and unidentified, where company.domain is set only for company. The page does not list every response field or the authentication details here, so follow its documentation and the developer sign-up pages for credentials. The sketch below keeps the parsing defensive.
def classify(payload):
verdict = payload.get("verdict", "unidentified")
if verdict == "company":
company = payload.get("company") or {}
return ("company", company.get("name"), company.get("domain"))
return (verdict, None, None)
The field names verdict, company and domain follow the verdict names in that page’s description; confirm exact response structure in the official documentation before relying on it. Whatever the vendor, anything other than a company verdict should be treated as unknown.
Key takeaway: write the code around outcomes, not around success. On typical B2B traffic most addresses will not resolve to a company, so non-company results are the normal path.
Which function answers which question
| Function | Question answered | Common result | Failure mode |
|---|---|---|---|
clean |
Is this a public, valid address? | Normalised string or None |
Rejects private and malformed input |
ptr |
What hostname does the holder publish? | A name or None |
No record is common |
rdap |
Who holds the block? | Label, range, country | Provider names on shared space |
classify |
Is it a business network? | Verdict plus company and domain | Mostly non-company outcomes |
Put it together with a cache
import json, time, pathlib
CACHE = pathlib.Path("ip_cache.json")
TTL = 14 * 24 * 3600
cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
def lookup(ip):
ip = clean(ip)
if not ip:
return None
hit = cache.get(ip)
if hit and time.time() - hit["t"] < TTL:
return hit["v"]
value = {"ptr": ptr(ip), "rdap": rdap(ip)}
cache[ip] = {"t": time.time(), "v": value}
return value
Save the cache periodically with CACHE.write_text(json.dumps(cache)). The two-week expiry reflects that holders change. Adjust it to your needs. For batch processing, deduplicate first and process in chunks; see bulk reverse IP lookup.
Concurrency and limits
Lookups spend most of their time waiting, so a small thread pool, perhaps four to eight workers, speeds a run noticeably. Use concurrent.futures.ThreadPoolExecutor and keep timeouts short. Do not push registries or APIs harder than their published limits; for paid APIs, add a budget guard that stops the run when spending reaches a ceiling. If you hit failures, back off with increasing delays instead of retrying instantly.
Testing your code
Test with addresses reserved for documentation, such as those in RFC 5737, and with known cases: a public DNS resolver address with a PTR record, an address with none, a malformed string, a private address and an IPv6 address. Check that none of them crash the program. To see what a company-level answer looks like before you write the API call, paste a few addresses into BusinessMCP’s free reverse IP lookup tool and compare. Its page states a fair-use limit of 10 lookups per hour for the free version.
Privacy reminders
Addresses can be personal data in some regions, so store only what you need and delete raw addresses when finished. If you send addresses to a third-party API, check its terms. The privacy guide summarises the main points, and the API overview covers what to evaluate before integrating. For the underlying concepts, see what a reverse IP lookup is.
Common pitfalls in Python lookups
Blocking calls without timeouts. gethostbyaddr uses the system resolver and can hang on slow servers. Set a default timeout and, for large batches, run lookups in a pool so one slow address does not stall everything.
Catching too little. Reverse DNS can raise herror, gaierror or a plain OSError, depending on the platform. Catch the family you expect and log anything else instead of letting a long job die at address 9,000.
Trusting hostnames. A PTR name is chosen by the holder. If a decision depends on it, such as recognising a crawler, confirm it with a forward lookup, and compare against the vendor’s published ranges where they exist.
Ignoring IPv6. The ipaddress module handles both families, but some downstream tools do not. Test with at least one IPv6 address.
Storing everything forever. Raw addresses can be personal data in some regions. Keep the cache small, expire entries and delete the file when the project ends.
Over-reading a result. A provider name from RDAP or a non-company verdict is a complete, correct answer. Do not write fallback logic that guesses a company from a hostname fragment, because it creates confident errors.
With these habits the code stays short, predictable and honest about what an IP address can reveal.
Frequently asked questions
How do I do a reverse DNS lookup in Python?
Use socket.gethostbyaddr(ip), which returns the hostname or raises socket.herror when no PTR record exists.
How do I get the owner of an IP in Python?
Query an RDAP service over HTTPS and parse the JSON, or call a company-level API that returns a verdict and company name.
Should I use threads for bulk lookups?
Modest concurrency helps because lookups wait on the network, but respect rate limits and keep timeouts short.
How do I avoid crashing on addresses with no answer?
Treat ‘no record’ as a normal result: catch the exception, store an explicit unknown value and move on.
Sources
- Python Software Foundation – socket: Low-level networking interface – https://docs.python.org/3/library/socket.html
- Python Software Foundation – ipaddress: IPv4/IPv6 manipulation library – https://docs.python.org/3/library/ipaddress.html
- IETF – RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) – https://www.rfc-editor.org/rfc/rfc7483