Skip to main content

netimps

Version Python versions License: MIT Docs CI

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, via getifaddrs(3) / GetAdaptersAddresses. No ifaddr required.
  • One parsing entry point — parse(value, type) with non-raising try_parse and boolean is_valid siblings. Type-form overloads preserve the parsed result, including v4/v6 union aliases and caller-supplied defaults.
  • MACAddress — colon/hyphen/dot/bare plus int/bytes, hashable and ordered, with .oui, .is_multicast, .is_local and case-selectable rendering.
  • FQDN — a domain name as a value type: .labels, .hostname, .domain, .tld, / to compose, plus .resolve()/.ping(). Path-like, but inverted from pathlib because DNS puts the significant label last — .name is the leftmost label and / prepends. name in domain works like the stdlib's address in network, and f + "/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, and is_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 — collapse and subtract, the latter missing from ipaddress entirely.
  • split_host — host:port splitting that gets IPv6 brackets right.
  • Scanning — concurrent scan_ports / scan_hosts.
  • Socket setup — bind() with the options named, UDPEndpoint for UDP servers that need to know which interface a datagram arrived on, and retry() for the backoff loop everyone writes without jitter.
  • Multicast — multicast_socket handling the join dance whose failure modes are otherwise silent.
  • DNS and ping — resolve() chaining dnspython/OS resolver/nslookup backends (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)

# 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_loopback is never derived from the name — lo, lo0 and Loopback Pseudo-Interface 1 share no spelling.
  • Interface.is_up says whether an interface is usable (IFF_UP and IFF_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 the 169.254 address 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/24 normalises instead of raising. Pass strict=True for stdlib behaviour.
  • Addresses can belong to more than one interface. get_interface() keeps the historical first-match result; use iter_interfaces() when duplicates such as unscoped IPv6 link-local assignments matter.
  • resolve raises 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. Windows ping exits 0 for "TTL expired in transit", so the reply address is verified instead of trusting the exit status.
  • count_hops works unprivileged, falling back to the system traceroute when a raw socket is unavailable.
  • discover_mtu measures; get_pmtu only reports what the kernel cached. The latter is usually None, and always None on 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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for netimps 0.4.0
File Size Uploaded
netimps-0.4.0.tar.gz 574.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for netimps 0.4.0
File Interpreter ABI Platform
netimps-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 936.3 kB

Release files / netimps-0.4.0.tar.gz

Download URL netimps-0.4.0.tar.gz
Size 574.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a78b8344bed99886f7f91f348815942e88944a11be61c8604a71ef4b95d78e34
BLAKE2b-256 checksum
How to use checksums
885e689b27a30ea9379ba568208c8c42d9d1b29f5e7a8b87f2f6028bd6386ee0
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 8, 2026.

Transparency log

Release files / netimps-0.4.0-py3-none-any.whl

Download URL netimps-0.4.0-py3-none-any.whl
Size 362.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
eebabe678e9a80e23d0ecac6bb8ecce05969ee377999337d02a612cef6d87137
BLAKE2b-256 checksum
How to use checksums
106dc4fa3c24b44dce38efb4028620af3e8b309b6ca254d47a9cf1f6e8976d35
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 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.1

2 release files

This release

0.4.0 This release

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.2

2 release files

0.0.1

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page