<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Python on Vedant Andhale</title>
    <link>https://www.vedant.me/tags/python/</link>
    <description>Recent content in Python on Vedant Andhale</description>
    <image>
      <url>https://www.vedant.me/</url>
      <link>https://www.vedant.me/</link>
    </image>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 10 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.vedant.me/tags/python/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AmbitionBox salary scraper: from embedded page data to usable records</title>
      <link>https://www.vedant.me/projects/ambitionbox-scraper/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/ambitionbox-scraper/</guid>
      <description>A Python salary-data pipeline with a custom Nuxt payload parser, HTML fallback, batch collection and pandas-based cleaning.</description>
      <content:encoded><![CDATA[<p>Salary comparisons need more context than a role name and a number. Company, location, experience range and the number of reports all affect how a record should be read.</p>
<p>I built the AmbitionBox salary scraper to collect that context into a consistent dataset. The Python pipeline discovers companies, extracts role-level salary records, writes batches and merges them into an analysis-ready CSV.</p>
<h2 id="the-parsing-work">The parsing work</h2>
<p>The most interesting part was the page&rsquo;s embedded Nuxt data. It is not a plain JSON object: the payload uses a self-invoking JavaScript function, arguments and variable references to construct records.</p>
<p>The parser identifies the function boundaries, tracks nested delimiters and quoted strings, maps parameter names to argument values, and resolves the property assignments used by the job-profile records. It extracts the supported structure rather than executing the page&rsquo;s JavaScript.</p>
<p>That distinction also defines a limitation: this is a parser for an observed payload format, not a general JavaScript interpreter. A change in the source representation can require an update.</p>
<h2 id="a-fallback-with-honest-missing-values">A fallback with honest missing values</h2>
<p>If the structured extraction returns no role records, the scraper falls back to the HTML table. That path can recover visible salary ranges and experience information, while leaving unavailable average-salary values empty.</p>
<p>An absent average should not become zero or an invented midpoint. Keeping it missing lets the later analysis distinguish between “this value was reported” and “this field was unavailable.”</p>
<table>
	<thead>
			<tr>
					<th>Stage</th>
					<th>Responsibility</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Discovery</td>
					<td>Build the company list used by subsequent batches.</td>
			</tr>
			<tr>
					<td>Extraction</td>
					<td>Parse role records, with optional location and role filters.</td>
			</tr>
			<tr>
					<td>Batch execution</td>
					<td>Split work through a GitHub Actions matrix and collect CSV artifacts.</td>
			</tr>
			<tr>
					<td>Merge</td>
					<td>Deduplicate records and normalise numeric and text columns with pandas.</td>
			</tr>
			<tr>
					<td>Derived fields</td>
					<td>Calculate salary in lakhs, experience midpoint and salary-range width.</td>
			</tr>
	</tbody>
</table>
<h2 id="give-the-merged-dataset-a-clear-identity">Give the merged dataset a clear identity</h2>
<p>The merge step uses company slug, role slug and location as the deduplication key. It keeps the last matching record in the concatenated input, converts numeric fields and sorts the output for inspection.</p>
<p>That is a concrete policy, not proof that a retained row is the newest observation. Adding an explicit collection timestamp would make that distinction easier to handle in future versions.</p>
<p>The workflow can collect available batch artifacts even when some jobs fail. A merged file therefore needs a completeness check before being treated as full coverage. I would add an expected-versus-received batch manifest and parser success counts as the next operational improvements.</p>
<h2 id="what-is-complete">What is complete</h2>
<p>The repository contains the collection, parsing and merge pipeline. Its exploratory notebook and interactive dashboard are described as work in progress, so I do not present them as finished deliverables.</p>
<p>This is an educational data-engineering project. The dataset reflects the source&rsquo;s salary reports; it is not verified payroll data or a representative survey of every employer. The portfolio value is the engineering: handling a nontrivial source format, preserving missing values and producing records that can be inspected and analysed.</p>
<p>Implementation details: <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/src/scraper/parser.py">payload parser</a>, <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/src/scraper/salary.py">salary collection</a>, <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/scripts/merge_data.py">merge logic</a>, and <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/.github/workflows/scrape.yml">batch workflow</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Debugging by narrowing the question</title>
      <link>https://www.vedant.me/notebook/debugging-without-a-debugger/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/notebook/debugging-without-a-debugger/</guid>
      <description>A practical approach to reproductions, hypotheses and boundary checks when stepping through the process is not available.</description>
      <content:encoded><![CDATA[<p>When a debugger is unavailable, it is tempting to scatter print statements through every suspicious function. The output grows quickly, but the question often stays vague: why is this broken?</p>
<p>A better first move is to write down the smallest observable mismatch. “The API is wrong” is broad. “This request returns an empty list when the database contains one matching row” gives the investigation a boundary.</p>
<h2 id="keep-one-reproduction-still">Keep one reproduction still</h2>
<p>Save the request, relevant configuration and expected result. Remove unrelated inputs until the failure stops shrinking. If the problem is intermittent, record what changes between runs instead of silently treating one successful run as a fix.</p>
<p>Then choose one hypothesis. Perhaps the identifier changes type between request parsing and the database query. Check the value and type at those two boundaries. If both are correct, that hypothesis has earned its retirement.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;lookup input: request_id=</span><span class="si">%s</span><span class="s2"> customer_id_type=</span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">request_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nb">type</span><span class="p">(</span><span class="n">customer_id</span><span class="p">)</span><span class="o">.</span><span class="vm">__name__</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This example deliberately records the type without dumping the entire request. Inspect only the fields needed to answer the question; a diagnostic log should not become a copy of every user&rsquo;s data.</p>
<h2 id="find-the-first-wrong-state">Find the first wrong state</h2>
<p>Trace a short chain: input, parsed value, query parameters, query result, response. If the query result is already empty, changing response serialization is unlikely to help.</p>
<p>For a longer pipeline, inspect the middle first. A correct midpoint moves the search downstream; an incorrect midpoint moves it upstream. You do not need a trace of every line to eliminate half the possibilities.</p>
<p>Once the cause is understood, turn the reproduction into a regression check when the failure is worth protecting against. Remove temporary noise and retain the diagnostic context that would make the same incident understandable next time.</p>
<p>The useful output of debugging is an explanation: this input met this condition, which caused this wrong result. A code change should follow from that explanation.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>loadfile: one call to load tabular data</title>
      <link>https://www.vedant.me/projects/loadfile/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/loadfile/</guid>
      <description>A small Python package that loads local and cloud files into pandas through a consistent API, replacing repeated file-loading code.</description>
      <content:encoded><![CDATA[<p>I built <strong>loadfile</strong> to stop copying the same file-loading code between scripts. It provides one entry point for tabular data, whether the file is on local disk or in cloud storage.</p>
<h2 id="one-small-api">One small API</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">loadfile</span> <span class="kn">import</span> <span class="n">load</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;data/local.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;gs://my-bucket/data.parquet&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;archive.zip&#34;</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="s2">&#34;sales.csv&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The function is named <code>load()</code>. The package also exports <code>load_data()</code> as a backwards-compatible alias.</p>
<p><code>fsspec</code> selects the storage backend from the path prefix. The package selects the reader from the file extension, or an explicit <code>format=</code> argument, and passes reader options through to pandas. Cloud backends require their corresponding optional dependencies and credentials.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;export.tsv&#34;</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s2">&#34;csv&#34;</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s2">&#34;</span><span class="se">\t</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;large.csv&#34;</span><span class="p">,</span> <span class="n">fast</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">usecols</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;id&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">])</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>CSV, Parquet, JSON, Excel and Feather share the same interface. <code>fast=True</code> opts into Arrow-backed reading; it changes the reading defaults rather than promising a fixed speedup for every file.</p>
<h2 id="zip-files-without-another-helper">ZIP files without another helper</h2>
<p>A ZIP containing one supported data file returns a DataFrame. Multiple supported members return a dictionary keyed by filename. You can select one member by name or pass a list to load a subset.</p>
<p>The implementation reads ZIP contents into memory, so archive size still matters. The aim is a convenient reusable loader, not an out-of-core processing engine.</p>
<p>Source: <a href="https://github.com/VedantAndhale/loadfile/blob/main/src/loadfile/__init__.py">public API</a> and <a href="https://github.com/VedantAndhale/loadfile/blob/main/src/loadfile/core.py">loading implementation</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Names should carry the missing context</title>
      <link>https://www.vedant.me/notebook/on-naming-things/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/notebook/on-naming-things/</guid>
      <description>Units, state and domain meaning make names useful; extra syllables alone do not.</description>
      <content:encoded><![CDATA[<p><code>timeout = 30</code> leaves an important question unanswered. Thirty milliseconds or thirty seconds? A comment can explain it, but <code>timeout_seconds</code> carries the answer wherever the value is used.</p>
<p>The useful part of a name is the context it saves the reader from reconstructing.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># These values have different meanings.</span>
</span></span><span class="line"><span class="cl"><span class="n">estimated_delivery_days</span> <span class="o">=</span> <span class="mi">4</span>
</span></span><span class="line"><span class="cl"><span class="n">scheduled_delivery_days</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"><span class="n">delay_days</span> <span class="o">=</span> <span class="n">estimated_delivery_days</span> <span class="o">-</span> <span class="n">scheduled_delivery_days</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Calling all three values variations of <code>delivery</code> would hide the relationship. Naming the unit and the kind of value makes the subtraction readable without opening another file.</p>
<h2 id="distinguish-a-fact-from-an-estimate">Distinguish a fact from an estimate</h2>
<p>This matters especially around model outputs and business calculations. <code>savings</code> can sound like money already saved. <code>estimated_savings</code> leaves room for the assumptions behind the number. <code>model_confidence</code> should not silently become <code>accuracy</code> when it reaches the interface.</p>
<p>The same care helps with state. A job can be accepted, queued, running or completed. A boolean named <code>done</code> often compresses several of those states and leaves failure behaviour unclear.</p>
<h2 id="match-the-scope">Match the scope</h2>
<p>A short local name can be perfectly clear inside a three-line loop. A public function parameter needs more context because callers see it without the implementation beside it.</p>
<p>Renaming is also a chance to notice a confused abstraction. If a helper needs a name such as <code>validate_and_save_and_maybe_notify</code>, perhaps the problem is the number of responsibilities, not your vocabulary.</p>
<p>I would rather use a slightly longer name with a precise meaning than a polished domain term that nobody on the team uses. The goal is for the next reader to make the right prediction about the code before running it.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Write logs for the person investigating a failure</title>
      <link>https://www.vedant.me/notebook/logs-nobody-reads/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/notebook/logs-nobody-reads/</guid>
      <description>Useful log events identify the operation, outcome and correlation context without copying the whole request.</description>
      <content:encoded><![CDATA[<p><code>Something went wrong</code> is a log message, but it leaves the next person with almost all the original work. Which operation failed? Was it a timeout or a rejected request? Can the event be connected to what the user saw?</p>
<p>A useful event gives the investigation a place to start. For an external API call, that usually means the operation, a correlation identifier, elapsed time, outcome and an error category.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">evaluate_shipment</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="ne">TimeoutError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;shipment evaluation timed out request_id=</span><span class="si">%s</span><span class="s2"> provider=</span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">request_id</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">provider_name</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The code is illustrative. In a real service, structured fields are easier to query than values embedded in a sentence, if the logging setup supports them.</p>
<h2 id="give-levels-a-consistent-meaning">Give levels a consistent meaning</h2>
<p>If every successful request produces a warning, a warning stops being useful. Decide which events represent normal operation, recoverable trouble and a failed operation that needs attention. Keep that meaning consistent across the service.</p>
<p>An exception stack is useful where the exception is handled and the context is known. Logging the same exception at every layer can turn one failure into a wall of repeated traces.</p>
<h2 id="leave-out-what-the-investigation-does-not-need">Leave out what the investigation does not need</h2>
<p>Full request bodies are an expensive default. They can include tokens, personal information and long documents. An identifier or a count may answer the debugging question without recording the underlying content.</p>
<p>Similarly, a success event should not claim more than happened. “Request accepted” and “job completed” are different events if a worker performs the job later.</p>
<p>Before adding a log line, imagine the query that would find it. If the line cannot help connect a symptom to an operation, it may belong in a temporary local diagnostic rather than the permanent event stream.</p>
<p>Reference: <a href="https://docs.python.org/3/howto/logging.html">Python logging HOWTO</a>.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
