<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pranjal Yadav</title>
    <description>The latest articles on DEV Community by Pranjal Yadav (@pranjal_yadav_4177fbc7c99).</description>
    <link>https://dev.to/pranjal_yadav_4177fbc7c99</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4104965%2Ff84fdecd-3ccc-4417-8f5e-dfa7ad208bf3.png</url>
      <title>DEV Community: Pranjal Yadav</title>
      <link>https://dev.to/pranjal_yadav_4177fbc7c99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranjal_yadav_4177fbc7c99"/>
    <language>en</language>
    <item>
      <title>Rebuilding Python's requests from Scratch (And What I Learned)</title>
      <dc:creator>Pranjal Yadav</dc:creator>
      <pubDate>Tue, 01 Sep 2026 19:47:09 +0000</pubDate>
      <link>https://dev.to/pranjal_yadav_4177fbc7c99/rebuilding-pythons-requests-from-scratch-and-what-i-learned-39p8</link>
      <guid>https://dev.to/pranjal_yadav_4177fbc7c99/rebuilding-pythons-requests-from-scratch-and-what-i-learned-39p8</guid>
      <description>&lt;p&gt;&lt;strong&gt;I recently built a zero-dependency HTTP client in Python from scratch. No requests, no httpx, not even urllib. Just raw sockets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Normally, I’d just &lt;strong&gt;pip install requests&lt;/strong&gt; and move on. It’s so standard that we forget what it actually does. Replacing it meant going back to basics: &lt;em&gt;opening raw TCP connections, manually encoding the HTTP request line, and parsing raw byte streams by hand.&lt;/em&gt;&lt;br&gt;
Here is what the code actually looks like when we strip away the abstraction, and what I learned in the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#.&lt;/strong&gt; &lt;strong&gt;The Hidden Stdlib Gem: Trusting TLS&lt;/strong&gt;-If we do networking in Python, we've probably seen &lt;strong&gt;certifi&lt;/strong&gt; installed to handle root certificates. I assumed a third-party CA bundle was mandatory. It turns out, it isn't. The &lt;strong&gt;ssl module&lt;/strong&gt; has a hidden gem:** ssl.create_default_context()**.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. Open the raw TCP connection
&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Wrap it in TLS using the OS native trust store
&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;sock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap_socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server_hostname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Gotcha:&lt;/strong&gt; The Python docs make wrapping a socket seem easy. What they don't emphasize is that if we just instantiate a base &lt;strong&gt;ssl.SSLContext()&lt;/strong&gt;, it loads absolutely zero trusted certificates by default. Every single HTTPS request will silently fail with an unknown-CA error. &lt;strong&gt;create_default_context()&lt;/strong&gt; automatically loads our operating system's native trust store and handles hostname verification right out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#.&lt;/strong&gt;  &lt;strong&gt;Building the Request is Just Strings&lt;/strong&gt;- When we call &lt;strong&gt;requests.get(url)&lt;/strong&gt;, it hides the fact that &lt;strong&gt;HTTP/1.1&lt;/strong&gt; is just a formatted text document sent over a wire. Building it by hand removes the magic. It's just a list of strings joined by carriage returns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; HTTP/1.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Host: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connection: close&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Encoding: identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Disable gzip so we read raw bytes
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent: httpc/1.0 (zero-dep)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;""&lt;/span&gt; &lt;span class="c1"&gt;# Blank line terminates headers
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# The protocol is just ascii strings joined by carriage returns
&lt;/span&gt;&lt;span class="n"&gt;raw_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latin-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_headers&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing this run successfully against a real API makes us realize how simple the web fundamentally is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#.&lt;/strong&gt; &lt;strong&gt;The Hard Part&lt;/strong&gt;: Chunked Transfer-Encoding This is the part that turned out much harder than the RFC docs made it look. &lt;strong&gt;requests&lt;/strong&gt; silently pieces chunked bodies together for us. When a server sends &lt;strong&gt;Transfer-Encoding&lt;/strong&gt;: chunked, we don't get a nice, &lt;strong&gt;clean Content-Length&lt;/strong&gt;. We get chunks of data prefixed by their size in hexadecimal.&lt;/p&gt;

&lt;p&gt;Doing this manually means writing a loop to parse hex-strings from a raw byte buffer just to figure out how many bytes to read next before the connection closes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A simplified look at manually parsing chunked encoding
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Find the next carriage return
&lt;/span&gt;    &lt;span class="n"&gt;crlf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Extract the hex string and parse it into an integer
&lt;/span&gt;    &lt;span class="n"&gt;size_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;crlf&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# The server is done sending data
&lt;/span&gt;
    &lt;span class="c1"&gt;# Now read exactly `chunk_size` bytes from the socket...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s tedious. We have to handle buffer boundaries, strip chunk extensions, and manage state manually. We quickly appreciate why we use libraries for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#.&lt;/strong&gt; &lt;strong&gt;The Honest Reality:&lt;/strong&gt; We didn't fully replace &lt;strong&gt;requests&lt;/strong&gt; to be clear, this project isn't a 1:1 replacement for requests or httpx. We didn't implement connection pooling, we didn't add HTTP/2 support, and we explicitly disabled gzip decompression.&lt;/p&gt;

&lt;p&gt;But replacing a library completely wasn't the point. The goal was to build a thin enough slice of the protocol to prove that we can do it, and to understand what the library is actually doing for us under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Real Takeaway&lt;/strong&gt; Abstractions are great for shipping, but terrible for understanding.&lt;/p&gt;

&lt;p&gt;When we just pass &lt;strong&gt;allow_redirects=True&lt;/strong&gt; into a library, HTTP routing remains magic. When we have to manually parse a 302 Location header, check the RFC rules, and recursively write the code to downgrade a POST to a GET, the magic disappears and becomes engineering.&lt;/p&gt;

&lt;p&gt;If we ever feel stuck in the cycle of just gluing APIs together, rewriting a core tool we use every day using nothing but standard tools is a great exercise. We obviously won't ship it to production, but the mental model we walk away with is permanent.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Check out the code here:&lt;/strong&gt; &lt;a href="https://github.com/PranjaldevX/httpc" rel="noopener noreferrer"&gt;https://github.com/PranjaldevX/httpc&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>zerodependencies</category>
    </item>
  </channel>
</rss>
