<aside>
💡
Scapy is the closest thing Python has to a packet workbench: you can build packets as data structures, send them, capture real traffic, and save the results as PCAP files for Wireshark.
</aside>
Networking can feel abstract at first. We draw headers as rows of boxes, memorize protocol numbers, and talk about packets moving through systems we cannot see. Scapy changes that. With a few lines of Python, an IP header stops being a diagram and becomes an object you can create, inspect, modify, and—when appropriate—put on the wire.
That makes Scapy especially useful in an teaching networking courses. It is small enough to explore interactively, but powerful enough to create realistic traffic and packet captures. Better still, it produces PCAP files, giving Wireshark a natural role in the same workflow.
What follows is not a complete Scapy tutorial. It is a short tour of the features I find most useful when introducing packets: building one, exchanging one, creating a trace, and capturing live traffic.
<aside>
⚠️
Generate or replay traffic only on networks you own or where you have explicit permission. A lab VM network or loopback interface is the safest place to experiment. Sniffing often requires elevated privileges on macOS and Linux.
</aside>
Getting Scapy running
Scapy requires Python 3 and is easy to install with pip:
python3 -m pip install scapy
A quick version check confirms that the installation worked:
python3 -c "from scapy.all import *; print(conf.version)"
Wireshark is not required, but it completes the experience. Scapy is excellent for creating and manipulating packets; Wireshark is excellent for exploring them visually.
Turning a header diagram into an object
The first useful Scapy session does not need to send anything. In a Python REPL, create an IP packet carrying an ICMP message:
from scapy.all import IP, ICMP
p = IP(dst="8.8.8.8")/ICMP()
print(p)
The slash is Scapy’s layering operator. Here it places ICMP inside IP, much as an encapsulation diagram would. The difference is that this diagram is executable.
Calling show() reveals the packet field by field:
p.show()
Individual values are available directly:
p[IP].dst
p[IP].ttl
p[ICMP].type
The packet can also become the bytes that would travel across a network:
raw_bytes = bytes(p)
len(raw_bytes)
raw_bytes[:20]
This is the moment when Scapy earns its place in the classroom. “The IP header” is no longer only a concept from a slide. It is structured data with fields, defaults, and a binary representation.
A packet exchange in a few lines
The smallest complete networking story is a request followed by a response. An ICMP echo provides exactly that:
from scapy.all import IP, ICMP, sr1
req = IP(dst="8.8.8.8")/ICMP()
reply = sr1(req, timeout=2, verbose=False)
if reply is None:
print("No reply")
else:
print("Request:")
req.show()
print("Reply:")
reply.show()
The sr1() function sends one packet and waits for one answer. In a controlled lab, I usually replace 8.8.8.8 with a gateway or server VM that belongs to the lab. The destination is less important than the symmetry: Scapy lets you place the request and response beside each other and see which fields correspond.
A timeout is instructive, too. “No reply” does not necessarily mean the destination is offline. A firewall may block ICMP, a route may be missing, or the response may simply arrive too late. Even this tiny program opens the door to an important networking habit: observed behavior must be interpreted, not merely recorded.
Building a trace before capturing one
We often use “trace” to mean a time-ordered sequence of packets. Scapy can create such a sequence entirely offline, which is useful when every student should begin with the same artifact.
Consider a set of UDP packets whose TTL values increase from one through five:
from scapy.all import IP, UDP
pkts = []
for ttl in range(1, 6):
p = IP(dst="203.0.113.1", ttl=ttl)/UDP(dport=33434)
pkts.append(p)
pkts[0].show()
len(pkts)
The destination address comes from a block reserved for documentation, so this code is best understood as packet construction rather than an invitation to transmit. Each packet is almost identical to the one before it; only the TTL changes. That small variation makes the sequence easy to reason about and connects naturally to a later discussion of traceroute.
Now write the packets to a PCAP file:
from scapy.all import wrpcap
wrpcap("ttl-demo.pcap", pkts)
print("Wrote ttl-demo.pcap")
Opening ttl-demo.pcap in Wireshark reveals the same packets through a different lens. Scapy emphasizes construction and code; Wireshark emphasizes comparison and visual inspection. Moving between the two helps connect Python objects, protocol fields, and bytes on disk.
This is one of my favorite ways to begin packet analysis. Everyone works from the same clean trace, and no one has to troubleshoot capture permissions before seeing something useful.
Adding an application-layer protocol
DNS makes the next step especially clear because it is familiar, structured, and layered above UDP and IP. Here is a DNS query for example.com:
from scapy.all import IP, UDP, DNS, DNSQR, sr1
q = IP(dst="8.8.8.8")/UDP(dport=53)/DNS(rd=1, qd=DNSQR(qname="example.com"))
a = sr1(q, timeout=2, verbose=False)
if a:
a.show()
That single expression makes the protocol stack visible: IP contains UDP, and UDP carries DNS. If a reply arrives, Scapy parses the payload back into meaningful fields rather than leaving it as an unexplained string of bytes.
For example, the query name is available as a[DNS].qd.qname. The DNS flags reveal whether recursion was requested and provided, while the answer section may contain A, AAAA, or CNAME records. At this point, layering becomes more than a diagram: each layer is both independent and connected to the others.
Moving from generated packets to live traffic
Once offline traces make sense, capturing live traffic feels like a natural next step. Scapy’s sniff() function can collect a fixed number of packets or stop after a timeout:
from scapy.all import sniff
pkts = sniff(count=20, timeout=10)
print(len(pkts))
An unrestricted capture can become noisy quickly. A Berkeley Packet Filter narrows the stream before Scapy processes it. This example listens only for DNS over UDP:
pkts = sniff(count=50, timeout=15, filter="udp port 53")
print(len(pkts))
The result can be saved just like the trace we generated earlier:
from scapy.all import wrpcap
wrpcap("sniffed-dns.pcap", pkts)
The distinction between these two PCAPs is useful. ttl-demo.pcap contains packets deliberately constructed in code. sniffed-dns.pcap records traffic observed on an interface. Both are packet traces, but they tell different kinds of stories.
On macOS and Linux, live capture often requires elevated privileges. If Scapy reports a permission error, run the script with sudo in an appropriate lab environment or configure capture permissions according to local policy.
Replaying a saved trace
Scapy can also read packets from a PCAP and transmit them:
from scapy.all import rdpcap, send
pkts = rdpcap("ttl-demo.pcap")
send(pkts, verbose=False)
Replay is useful in a controlled environment when several people need to generate the same traffic pattern against a service they own. It also creates an opening for later discussions about timing, state, addresses, checksums, and why replayed traffic may not behave exactly like the original exchange.
Because replay places packets back on the network, it deserves the same caution as any other transmission: use it only in an isolated or explicitly authorized environment.
A small capture program worth keeping
After experimenting in the REPL, it helps to put one useful task into a complete script. The following program captures UDP traffic for up to 30 seconds and saves the result:
from scapy.all import sniff, wrpcap
def main():
pkts = sniff(count=100, timeout=30, filter="udp")
wrpcap("capture.pcap", pkts)
print(f"Saved {len(pkts)} packets to capture.pcap")
if __name__ == "__main__":
main()
It is intentionally modest: one file, one job, and one artifact to inspect. From here, it is easy to change the filter, packet count, timeout, or output filename without hiding the networking ideas beneath a larger application.
From diagrams to evidence
Scapy occupies a useful middle ground. It is approachable enough for a first networking course, yet it exposes the same packet structures that appear in serious testing, troubleshooting, and research.
The real value is not that Scapy makes it easy to send a ping or save a capture. It is that it shortens the distance between an idea and evidence. A header becomes an object. An object becomes bytes. A sequence of packets becomes a PCAP. That PCAP becomes something we can inspect, question, and explain.
For someone learning networking, that progression is far more memorable than another page of acronyms.
Selah.