netimps
The network utilities every tool ends up rewriting — interface discovery, "which is my IP", reachability checks, CIDR maths, host:port parsing, DNS, ping, scanning and multicast — as one typed, flat-import library.
Built on the standard library, with no hard runtime dependencies.
resolve() tries dnspython, the OS resolver and nslookup, in that order,
using whichever is available (pip install netimps[dns] for dnspython
itself, the fullest of the three: structured records, every record type,
explicit nameserver/search-list control). Interface enumeration, routing and
multicast are ctypes bindings to the platform's own APIs, so there is
nothing to compile and no wheel to miss for your platform.
Features
- Interface discovery, no dependencies —
get_interfaces()gives adapter names, MACs, MTU and real prefix lengths on Linux, macOS/BSD and Windows, viagetifaddrs(3)/GetAdaptersAddresses. Noifaddrrequired. - One parsing entry point —
parse(value, type)with non-raisingtry_parseand booleanis_validsiblings. Type-form overloads preserve the parsed result, including v4/v6 union aliases and caller-supplied defaults. MACAddress— colon/hyphen/dot/bare plusint/bytes, hashable and ordered, with.oui,.is_multicast,.is_localand case-selectable rendering.FQDN— a domain name as a value type:.labels,.hostname,.domain,.tld,/to compose, plus.resolve()/.ping(). Path-like, but inverted frompathlibbecause DNS puts the significant label last —.nameis the leftmost label and/prepends.name in domainworks like the stdlib'saddress in network, andf + "/path"gives a plain string. Refuses an address literal;Host.fqdn()bridges the two.- The socket helpers everyone rewrites —
get_source_ip,get_free_port,tcp_check,wait_for_port. - Local membership lookups —
get_interface()gives the first adapter for an address, interface, network or MAC;iter_interfaces()yields every match, andis_local_address()distinguishes assignment from mere address scope. - Routing and MTU —
get_route(first hop, unprivileged),count_hops(raw sockets or traceroute fallback),discover_mtu/get_pmtu,Interface.mtu. - CIDR set maths —
collapseandsubtract, the latter missing fromipaddressentirely. split_host—host:portsplitting that gets IPv6 brackets right.- Scanning — concurrent
scan_ports/scan_hosts. - Socket setup —
bind()with the options named,UDPEndpointfor UDP servers that need to know which interface a datagram arrived on, andretry()for the backoff loop everyone writes without jitter. - Listening —
bind_listen()turns one argument ("*","[::1]:69","eth1:67", a MAC, a list of those) into boundUDPEndpoints, limited to the interfaces it names;parse_listen()is the same reading with no I/O. - Multicast —
multicast_sockethandling the join dance whose failure modes are otherwise silent. - DNS and ping —
resolve()chainingdnspython/OS resolver/nslookupbackends (plus a standard-library DNS client for an explicit nameserver or source address) and returning native types;resolve_doh()for DNS over HTTPS; accepting an IP/interface object directly and auto-selecting a reverse lookup for an address query;ping()returning round-trip time and TTL, not just a boolean.
Installation
pip install netimps # the library -- no hard dependencies
pip install netimps[dns] # plus dnspython, for resolve()'s fullest backend
pip install netimps[cli] # plus the `netimps` command
Requires Python 3.9+. Both extras are additive and independent; importing the library never requires either.
Quick start
import netimps
from netimps import IPAddress, IPNetwork, MACAddress, parse
# Interfaces: names, MACs, MTU and real prefixes on every OS
for iface in netimps.get_interfaces():
print(iface.name, iface.mac, iface.mtu, [str(ip) for ip in iface.ips])
# 'Wi-Fi' 00:00:5e:00:53:01 1500 ['192.0.2.10/24', 'fe80::.../64']
# Types annotate; parse builds
def route(dst: IPAddress, via: IPNetwork) -> None: ...
parse("10.0.0.5") # IPv4Address('10.0.0.5')
parse("10.0.0.5/24", IPNetwork) # IPv4Network('10.0.0.0/24')
netimps.try_parse("nope", IPAddress) # None
netimps.is_valid("::1", IPAddress) # True
# Which of my addresses actually reaches that host?
netimps.get_source_ip("8.8.8.8") # IPv4Address('192.0.2.10')
netimps.get_route("8.8.8.8").gateway # IPv4Address('192.0.2.1')
# Exact assignment, subnet membership, MAC ownership, name or index
netimps.get_interface("192.0.2.10") # first matching Interface, or None
netimps.get_interface("Wi-Fi") # by adapter name
netimps.get_interface(index=10) # by interface index
list(netimps.iter_interfaces(parse("192.0.2.0/24", IPNetwork)))
netimps.is_local_address("127.0.0.1") # True without interface discovery
# Honest reachability, and waiting for a service
netimps.tcp_check("example.com", 443) # True
netimps.wait_for_port("localhost", 5432, deadline=60)
# CIDR set maths
netimps.subtract(["10.0.0.0/24"], ["10.0.0.64/26"])
# [IPv4Network('10.0.0.0/26'), IPv4Network('10.0.0.128/25')]
# host:port, including the IPv6 case people get wrong
netimps.split_host("[::1]:8080") # ('::1', 8080)
netimps.split_host("::1") # ('::1', None) -- not port 1
# MAC addresses
mac = MACAddress("AA-BB-CC-DD-EE-FF")
mac.format("-", upper=True) # 'AA-BB-CC-DD-EE-FF'
mac.is_local, mac.oui.hex() # (True, 'aabbcc') -- AA has the U/L bit
# DNS returns native types
netimps.resolve("example.com")[0].is_global # an IPv4Address, not a str
netimps.resolve("example.com", "txt") # ['v=spf1 -all'] -- unquoted
# ping carries the details, and can use TCP or UDP where ICMP is blocked
result = netimps.ping("8.8.8.8")
result.ok, result.rtt, result.ttl # (True, 0.009, 119) rtt in seconds
netimps.ping("8.8.8.8", method="tcp", port=53) # times the handshake
# Path MTU, measured rather than guessed
netimps.discover_mtu("8.8.8.8") # 1500
netimps.discover_mtu("10.0.0.5", method="udp", port=9999)
netimps.get_tcp_mss("example.com", 443) # 1460
# Scanning and multicast
netimps.scan_ports("192.168.1.1", ["ssh", "https"]) # [22, 443]
sock = netimps.multicast_socket("224.0.0.251", 5353) # mDNS listener
# Server-side: bind with the options named, and know where packets came from
server = netimps.bind("", 6767, broadcast=True)
endpoint = netimps.UDPEndpoint(server)
# Where a service listens: one argument, read once, then bound as endpoints
netimps.parse_listen("eth1:67,[::1]:69") # no I/O: (0.0.0.0, 67, ('eth1',)), (::1, 69, ())
endpoints = netimps.bind_listen("127.0.0.1:6768") # one UDPEndpoint for each socket
# Retry with backoff and jitter
netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
Command line
netimps interfaces # names, MACs, MTU, addresses
netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
netimps resolve example.com aaaa
netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
netimps check example.com https # port number or scheme name
netimps mtu 8.8.8.8 # measured, not guessed
netimps scan 192.0.2.0/29 -p common
netimps addr 00:00:5e:00:53:01 # address, network or MAC
netimps split '[::1]:8080' # -> ::1 8080
Every command takes --json. Exit statuses, one meaning each (as grep): 0
found or yes, 1 nothing found or the answer was no, 2 an error. -q prints
no result line and answers by the status alone, for scripts (--json is not
affected):
netimps check example.com https -q && echo reachable
netimps scan 192.0.2.0/29 -p 22 -q || echo nothing answers on 22
API overview
| Name | Purpose |
|---|---|
IPAddress, IPInterface, IPNetwork |
v4/v6 union aliases for annotations |
IPAddressLike, IPInterfaceLike, IPNetworkLike, MACAddressLike |
accepted-input unions |
HostLike |
accepted-input union for a single destination (hostname, address, or IPv4Interface/IPv6Interface — its .ip is used) |
IPv4Address, IPv4Interface, ... |
stdlib concrete-type re-exports |
parse, try_parse, is_valid |
build a type from a value (raising / None / bool) |
classify |
read text as a MAC, network, interface or address, whichever it is |
MACAddress |
parse / classify / render MAC addresses |
get_interfaces, Interface, iter_addresses |
native cross-platform NIC discovery |
is_link_scoped |
scope classification |
collapse, subtract |
CIDR set maths |
split_host, split_zone |
host:port (or (host, port)) splitting, IPv6-aware; the %zone of a scoped address |
join_host, unmap, is_wildcard |
build host:port (IPv6-bracketed), collapse a v4-mapped address, test for the bind-anything form |
get_default_port, get_default_scheme, register_port |
scheme ↔ port registry |
resolve, has_dns |
DNS lookup → native records ([] for no such record; with strict=True a resolver that could not be asked raises); cache= keeps answers, misses included; has_dns() asks for the dns extra |
clear_resolution_cache, RESOLUTION_CACHE_TTL |
drop the cache= answers; their default 30 s lifetime |
resolve_wire, resolve_doh |
DNS straight to a nameserver (UDP/TCP, chosen source) / over HTTPS |
ping, PingResult |
reachability with RTT and TTL |
bind, bind_error_hint, get_interface, iter_interfaces, is_local_address, is_local_host |
socket creation and local membership |
get_source_ip, get_free_port, tcp_check, wait_for_port |
socket helpers |
UDPEndpoint, Datagram |
UDP receive with arrival interface (IP_PKTINFO), every platform; Datagram.destination and .is_unicast say where it was sent |
is_broadcast, is_unicast |
was a destination a broadcast / one host (not broadcast, multicast or the wildcard) |
recvmsg, sendmsg, CMSG_LEN, CMSG_SPACE |
ancillary-data messaging, Windows included |
Host |
hostname-or-address value type; .fqdn() narrows a name |
FQDN, FQDNLike |
domain name value type with label algebra (inverted from pathlib) |
retry, backoff_delays |
bounded retry with exponential backoff |
LINK_LOCAL_V4, LOOPBACK_V4, LOOPBACK_V6, LINK_LOCAL_V6 |
named networks |
get_route, Route, count_hops |
routing and distance |
discover_mtu, get_pmtu, get_tcp_mss |
path MTU by ICMP/UDP/TCP, the kernel's cached guess, or the negotiated MSS |
scan_ports, scan_hosts, PORT_RANGES |
concurrent scanning |
multicast_socket, join_group, leave_group, is_multicast |
multicast |
get_hostname() |
this machine's name, asked for when called; fqdn=True for the qualified one |
Full per-export reference, with contracts and gotchas, lives in
src/netimps/AGENTS.md,
which also ships beside this file inside the installed package.
Design notes
A few behaviours are deliberate and worth knowing:
Interface.is_loopbackis never derived from the name —lo,lo0andLoopback Pseudo-Interface 1share no spelling.Interface.is_upsays whether an interface is usable (IFF_UPandIFF_RUNNING, or the Windows operational status). On Windows an address the system marks tentative or duplicate is left out, so a media-disconnected adapter stays listed without the169.254address it cannot bind.- Concrete types are strict about family.
parse("::1", IPAddress)works;parse("::1", IPv4Address)raises rather than quietly returning v6. - Networks parse non-strict by default, so
10.0.0.5/24normalises instead of raising. Passstrict=Truefor stdlib behaviour. - Addresses can belong to more than one interface.
get_interface()keeps the historical first-match result; useiter_interfaces()when duplicates such as unscoped IPv6 link-local assignments matter. resolveraises on a malformed query rather than returning[]— a typo'd record type should not look like "no such record".ping()decides success from the reply, not the exit code. Windowspingexits0for "TTL expired in transit", so the reply address is verified instead of trusting the exit status.count_hopsworks unprivileged, falling back to the system traceroute when a raw socket is unavailable.discover_mtumeasures;get_pmtuonly reports what the kernel cached. The latter is usuallyNone, and alwaysNoneon Windows. On one real host the local link was 9000 and the true path MTU 1500 — only probing found it.
Development
Venvs are named .venv/<version>-<os>-<arch>/, one per interpreter tested
against — <arch> being what the interpreter was built for, not what the host
is. Substitute your own:
python -m venv .venv/3.14-nt-arm64
.venv/3.14-nt-arm64/Scripts/pip install -e ".[dev]" # POSIX: .../bin/pip
.venv/3.14-nt-arm64/Scripts/pytest -q
.venv/3.14-nt-arm64/Scripts/black src/ tests/
Tested on Python 3.9 (the floor) and 3.14.
Releasing
This project follows Semantic Versioning and keeps a
CHANGELOG.md.
Pushing a tag matching v* triggers the release workflow: test gate → build
→ publish → docs deploy.
License
MIT — see LICENSE.
Metadata
Release files for netimps 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| netimps-0.4.1.tar.gz | 602.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| netimps-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 979.4 kB
Release files / netimps-0.4.1.tar.gz
| Download URL | netimps-0.4.1.tar.gz |
|---|---|
| Size | 602.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
caefe5dd71c05440f3627843d590cbe2a5510819a035b95091dcb958b93cb157
|
|
BLAKE2b-256 checksum How to use checksums |
de1a218997b70a9a5e43b9557f06c129ec60b64ec672f16fda8602010edf6471
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / netimps-0.4.1-py3-none-any.whl
| Download URL | netimps-0.4.1-py3-none-any.whl |
|---|---|
| Size | 376.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b425c26973e2fd94adbca76245a7cc17da33317639363ef5c7ab146137bb37fb
|
|
BLAKE2b-256 checksum How to use checksums |
0c2bae9f23a95b145b220914b204048c939b34b87b97b06b9f55169a098cc58a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log