<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mahir Faysal — Dev Notes]]></title><description><![CDATA[Mahir Faysal — Dev Notes]]></description><link>https://mfaysaldev.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Mahir Faysal — Dev Notes</title><link>https://mfaysaldev.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 15 Oct 2026 12:26:06 GMT</lastBuildDate><atom:link href="https://mfaysaldev.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[MySQL GROUP_CONCAT with ORDER BY, SEPARATOR and DISTINCT (Tested on 8.4)]]></title><description><![CDATA[To join several rows into one string per group in MySQL, use GROUP_CONCAT() with GROUP BY. The order and the separator go inside the brackets: GROUP_CONCAT(subject ORDER BY subject SEPARATOR ', '). Ad]]></description><link>https://mfaysaldev.hashnode.dev/mysql-group-concat-with-order-by-separator-and-distinct-tested-on-8-4</link><guid isPermaLink="true">https://mfaysaldev.hashnode.dev/mysql-group-concat-with-order-by-separator-and-distinct-tested-on-8-4</guid><category><![CDATA[MySQL]]></category><category><![CDATA[SQL]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Mahir Faysal]]></dc:creator><pubDate>Thu, 15 Oct 2026 04:09:02 GMT</pubDate><content:encoded><![CDATA[<p>To join several rows into one string per group in MySQL, use <code>GROUP_CONCAT()</code> with <code>GROUP BY</code>. The order and the separator go <strong>inside</strong> the brackets: <code>GROUP_CONCAT(subject ORDER BY subject SEPARATOR ', ')</code>. Add <code>DISTINCT</code> straight after the opening bracket to drop duplicates. Two things catch people out: NULL values are skipped silently, and the result is cut off at <code>group_concat_max_len</code>, which is 1,024 bytes by default, with only a warning to tell you.</p>
<p>I ran every query in this post on <strong>MySQL 8.4.6</strong> on 11 October 2026, with the default <code>sql_mode</code> (which includes <code>ONLY_FULL_GROUP_BY</code>), and pasted the real output under each one. Everything below is copy-and-paste ready: create the two tables, insert the rows and run each query in order. If you run your queries from PHP, my guide to <a href="https://mfaysal.com/blog/php-pdo-fetch-assoc-fetchall">PDO fetch() and fetchAll()</a> shows how to read the results back.</p>
<h2>The sample tables</h2>
<p>The examples use two small tables: students and the subjects they’ve taken, with an optional score. I’ve deliberately included a duplicate (Chloe took Biology twice), a NULL score and a row with a NULL subject, because that’s where <code>GROUP_CONCAT</code> behaves in surprising ways.</p>
<pre><code class="language-sql">CREATE TABLE students (id INT PRIMARY KEY, name VARCHAR(40), class VARCHAR(10));
CREATE TABLE subjects (student_id INT, subject VARCHAR(40), score INT NULL);

INSERT INTO students VALUES
  (1,'Ayesha','10A'), (2,'Ben','10A'), (3,'Chloe','10B'), (4,'Dev','10B');

INSERT INTO subjects VALUES
  (1,'Maths',91), (1,'Physics',84), (1,'English',77),
  (2,'History',68), (2,'Maths',73),
  (3,'Biology',88), (3,'Maths',NULL), (3,'Biology',90),
  (4,NULL,NULL);
</code></pre>
<h2>Basic GROUP_CONCAT: one row per group</h2>
<p><code>GROUP_CONCAT()</code> is an aggregate function, like <code>COUNT()</code> or <code>SUM()</code>. The <a href="https://dev.mysql.com/doc/refman/8.4/en/aggregate-functions.html">MySQL aggregate function reference</a> describes it simply as “return a concatenated string”. You group the rows, and it glues each group’s values together.</p>
<pre><code class="language-sql">SELECT s.name, GROUP_CONCAT(sub.subject) AS subjects
FROM students s
JOIN subjects sub ON sub.student_id = s.id
GROUP BY s.id, s.name
ORDER BY s.id;
</code></pre>
<pre><code class="language-text">+--------+-----------------------+
| name   | subjects              |
+--------+-----------------------+
| Ayesha | Maths,Physics,English |
| Ben    | History,Maths         |
| Chloe  | Biology,Maths,Biology |
| Dev    | NULL                  |
+--------+-----------------------+
</code></pre>
<p>Notice three things. The default separator is a comma with <strong>no space</strong>. The order inside each list isn’t guaranteed; here it happens to follow insertion order, but you shouldn’t rely on that. And Dev’s only subject is NULL, so his whole result is NULL.</p>
<h2>GROUP_CONCAT with ORDER BY and SEPARATOR</h2>
<p>To control the order of values inside each string, put <code>ORDER BY</code> <strong>inside</strong> the function call. To change the separator, add <code>SEPARATOR</code> after it. Both are part of <code>GROUP_CONCAT</code>’s own syntax, separate from the query’s outer <code>ORDER BY</code>.</p>
<pre><code class="language-sql">SELECT s.name,
       GROUP_CONCAT(sub.subject ORDER BY sub.subject SEPARATOR ', ') AS subjects
FROM students s
JOIN subjects sub ON sub.student_id = s.id
GROUP BY s.id, s.name
ORDER BY s.id;
</code></pre>
<pre><code class="language-text">+--------+-------------------------+
| name   | subjects                |
+--------+-------------------------+
| Ayesha | English, Maths, Physics |
| Ben    | History, Maths          |
| Chloe  | Biology, Biology, Maths |
| Dev    | NULL                    |
+--------+-------------------------+
</code></pre>
<p>The outer <code>ORDER BY s.id</code> sorts the <strong>rows</strong> of the result. The inner <code>ORDER BY sub.subject</code> sorts the <strong>values within each string</strong>. You’ll often want both.</p>
<h3>Ordering by a different column</h3>
<p>The inner <code>ORDER BY</code> doesn’t have to use the column you’re concatenating. Here I list each student’s subjects best score first, with the score in brackets and a pipe as the separator:</p>
<pre><code class="language-sql">SELECT s.name,
       GROUP_CONCAT(CONCAT(sub.subject, ' (', sub.score, ')')
                    ORDER BY sub.score DESC SEPARATOR ' | ') AS best_first
FROM students s
JOIN subjects sub ON sub.student_id = s.id
GROUP BY s.id, s.name
ORDER BY s.id;
</code></pre>
<pre><code class="language-text">+--------+------------------------------------------+
| name   | best_first                               |
+--------+------------------------------------------+
| Ayesha | Maths (91) | Physics (84) | English (77) |
| Ben    | Maths (73) | History (68)                |
| Chloe  | Biology (90) | Biology (88)              |
| Dev    | NULL                                     |
+--------+------------------------------------------+
</code></pre>
<p>Look closely at Chloe. Her Maths entry has disappeared. Its score is NULL, <code>CONCAT()</code> returns NULL if any argument is NULL, and <code>GROUP_CONCAT</code> skips NULL values. That leads us to the biggest gotcha.</p>
<h2>Why NULL values disappear from GROUP_CONCAT</h2>
<p>The MySQL manual states that, unless otherwise stated, aggregate functions ignore NULL values, and <code>GROUP_CONCAT</code> follows that rule. If every value in a group is NULL, the result is NULL rather than an empty string.</p>
<pre><code class="language-sql">SELECT s.name, GROUP_CONCAT(sub.subject) AS subjects, COUNT(*) AS rows_in_group
FROM students s
LEFT JOIN subjects sub ON sub.student_id = s.id
WHERE s.id = 4
GROUP BY s.id, s.name;
</code></pre>
<pre><code class="language-text">+------+----------+---------------+
| name | subjects | rows_in_group |
+------+----------+---------------+
| Dev  | NULL     |             1 |
+------+----------+---------------+
</code></pre>
<p>There’s one row in the group, but its subject is NULL, so the concatenated result is NULL. Two fixes:</p>
<ul>
<li><strong>Default for the whole result:</strong> wrap it in <code>COALESCE(GROUP_CONCAT(sub.subject), 'none')</code>, which returned <code>none</code> for Dev in my test.</li>
<li><strong>Keep NULL items visible:</strong> use <code>COALESCE()</code> or <code>IFNULL()</code> on the value inside, for example <code>GROUP_CONCAT(CONCAT(sub.subject, ' (', COALESCE(sub.score, '-'), ')'))</code>, so Chloe’s Maths stays in the list instead of vanishing. In my test that returned <code>Biology (90) | Biology (88) | Maths (-)</code>. Note that NULL scores sort last with <code>DESC</code>, which is usually what you want.</li>
</ul>
<h2>GROUP_CONCAT DISTINCT: removing duplicates</h2>
<p>Put <code>DISTINCT</code> first inside the brackets to keep only unique values. You can combine it with <code>ORDER BY</code> and <code>SEPARATOR</code>:</p>
<pre><code class="language-sql">SELECT s.name,
       GROUP_CONCAT(DISTINCT sub.subject ORDER BY sub.subject SEPARATOR ', ') AS subjects
FROM students s
JOIN subjects sub ON sub.student_id = s.id
WHERE s.id = 3
GROUP BY s.id, s.name;
</code></pre>
<pre><code class="language-text">+-------+----------------+
| name  | subjects       |
+-------+----------------+
| Chloe | Biology, Maths |
+-------+----------------+
</code></pre>
<p>Biology now appears once. If duplicate rows are a symptom of bad data rather than a legitimate repeat, fix the table instead; my post on <a href="https://mfaysal.com/blog/mysql-delete-duplicate-rows">deleting duplicate rows in MySQL</a> covers safe ways to do that.</p>
<h2>Custom separators: no separator, new lines and more</h2>
<p>Any string literal works as a separator, including an empty one and escape sequences:</p>
<pre><code class="language-sql">SELECT class,
       GROUP_CONCAT(name ORDER BY name SEPARATOR '')   AS squashed,
       GROUP_CONCAT(name ORDER BY name SEPARATOR '\n') AS one_per_line
FROM students
GROUP BY class;
</code></pre>
<pre><code class="language-text">+-------+-----------+--------------+
| class | squashed  | one_per_line |
+-------+-----------+--------------+
| 10A   | AyeshaBen | Ayesha
Ben   |
| 10B   | ChloeDev  | Chloe
Dev    |
+-------+-----------+--------------+
</code></pre>
<p>The new-line version breaks the text table, which is exactly what you want when you’re building a plain-text email or a CSV cell. Other separators I find handy are <code>' | '</code> for readable reports and <code>';'</code> for exporting to spreadsheets that treat commas as column breaks.</p>
<h2>The 1024 limit: group_concat_max_len and truncated results</h2>
<p>This one causes real bugs in production. <code>GROUP_CONCAT</code> results are capped by the <code>group_concat_max_len</code> system variable. On my MySQL 8.4.6 server, <code>SELECT @@group_concat_max_len;</code> returned <strong>1024</strong>. When a result is longer, MySQL cuts it off and raises a warning, but the query still succeeds, so your application never notices.</p>
<p>To demonstrate, I set a tiny limit of 20 bytes:</p>
<pre><code class="language-sql">SET SESSION group_concat_max_len = 20;
SELECT GROUP_CONCAT(name ORDER BY id SEPARATOR ', ') AS names FROM students;
SHOW WARNINGS;
</code></pre>
<pre><code class="language-text">+----------------------+
| names                |
+----------------------+
| Ayesha, Ben, Chloe,  |
+----------------------+
+---------+------+---------------------------------+
| Level   | Code | Message                         |
+---------+------+---------------------------------+
| Warning | 1260 | Row 4 was cut by GROUP_CONCAT() |
+---------+------+---------------------------------+
</code></pre>
<p>Dev was silently dropped, and the string even ends with a stray separator. Raising the limit for the session fixes it:</p>
<pre><code class="language-sql">SET SESSION group_concat_max_len = 1000000;
SELECT GROUP_CONCAT(name ORDER BY id SEPARATOR ', ') AS names FROM students;
</code></pre>
<pre><code class="language-text">+-------------------------+
| names                   |
+-------------------------+
| Ayesha, Ben, Chloe, Dev |
+-------------------------+
</code></pre>
<p>Practical advice:</p>
<ul>
<li>Run <code>SET SESSION group_concat_max_len = ...</code> right after connecting, in the same connection that runs the query. It only lasts for that session.</li>
<li>If you can’t predict the size, check warnings (warning code 1260) or compare the result length with the limit.</li>
<li>If the list could be huge, ask whether you need one string at all. Fetching the rows and joining them in PHP or Python is often simpler and has no cut-off.</li>
</ul>
<h2>Top N per group with GROUP_CONCAT</h2>
<p>MySQL’s <code>GROUP_CONCAT</code> has no <code>LIMIT</code> clause inside the brackets (MariaDB added one, MySQL hasn’t), so a common trick is to order the list and then keep the first N items with <code>SUBSTRING_INDEX()</code>:</p>
<pre><code class="language-sql">SELECT s.name,
       SUBSTRING_INDEX(GROUP_CONCAT(sub.subject ORDER BY sub.score DESC SEPARATOR ','), ',', 2) AS top2
FROM students s
JOIN subjects sub ON sub.student_id = s.id
WHERE s.id = 1
GROUP BY s.id, s.name;
</code></pre>
<pre><code class="language-text">+--------+---------------+
| name   | top2          |
+--------+---------------+
| Ayesha | Maths,Physics |
+--------+---------------+
</code></pre>
<p><code>SUBSTRING_INDEX(str, ',', 2)</code> returns everything before the second comma. It breaks if the values themselves contain your separator, so pick one that can’t appear in the data. For anything more complex, a window function such as <code>ROW_NUMBER()</code> in a subquery is more robust.</p>
<h2>GROUP_CONCAT vs JSON_ARRAYAGG</h2>
<p>If the result is going to code rather than a human, consider <code>JSON_ARRAYAGG()</code>, which the MySQL manual lists as returning the result set “as a single JSON array”. You get proper quoting for free, so commas inside values can’t break your parsing:</p>
<pre><code class="language-sql">SELECT s.name, JSON_ARRAYAGG(sub.subject) AS subjects_json
FROM students s
JOIN subjects sub ON sub.student_id = s.id
WHERE s.id IN (1, 2)
GROUP BY s.id, s.name
ORDER BY s.id;
</code></pre>
<pre><code class="language-text">+--------+---------------------------------+
| name   | subjects_json                   |
+--------+---------------------------------+
| Ayesha | ["Maths", "Physics", "English"] |
| Ben    | ["History", "Maths"]            |
+--------+---------------------------------+
</code></pre>
<p>In PHP you can <code>json_decode()</code> that column straight into an array. The trade-off: <code>JSON_ARRAYAGG</code> doesn’t accept an <code>ORDER BY</code> inside the brackets. When I tried <code>JSON_ARRAYAGG(name ORDER BY name)</code> on MySQL 8.4.6, it failed with syntax error 1064. Use <code>GROUP_CONCAT</code> when the order of items matters, or sort the array in your application code after decoding it.</p>
<h2>Error 1055 and ONLY_FULL_GROUP_BY</h2>
<p>Because MySQL 8.4 enables <code>ONLY_FULL_GROUP_BY</code> by default, every non-aggregated column in your <code>SELECT</code> must be in the <code>GROUP BY</code> or depend on it. Grouping by the primary key <code>s.id</code> while selecting <code>s.name</code> is fine, since the name depends on the id. But this query fails:</p>
<pre><code class="language-sql">SELECT s.class, s.name, GROUP_CONCAT(sub.subject)
FROM students s
JOIN subjects sub ON sub.student_id = s.id
GROUP BY s.class;
</code></pre>
<pre><code class="language-text">ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains
nonaggregated column 'gc_demo.s.name' which is not functionally dependent on columns in
GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
</code></pre>
<p>Each class has several names, so MySQL can’t pick one. Either add <code>s.name</code> to the <code>GROUP BY</code> or aggregate it too, for example <code>GROUP_CONCAT(DISTINCT s.name)</code>. Don’t switch the SQL mode off to make the error go away; it’s protecting you from random results.</p>
<h2>When not to use GROUP_CONCAT</h2>
<p><code>GROUP_CONCAT</code> is brilliant for display: a tag list next to a blog post, a comma-separated list of products in an order summary, or a quick report you paste into a spreadsheet. It’s a poor fit when you need to filter, count or join on the individual values later. Searching inside a concatenated string with <code>LIKE '%Maths%'</code> is slow and matches partial words, and storing comma-separated lists in a column breaks the basic rule of one value per field. Keep the data in proper rows and only concatenate at the very end, in the query that feeds the page.</p>
<h2>Using GROUP_CONCAT safely from PHP</h2>
<p>If any part of the query comes from user input, such as a class name from a dropdown, bind it with a prepared statement rather than gluing it into the SQL string. My guide to <a href="https://mfaysal.com/blog/php-pdo-prepared-statements">PHP PDO prepared statements</a> shows the pattern. <code>GROUP_CONCAT</code> itself is just another column in the result; you read it with <code>fetch()</code> like any other value and <code>explode(', ', $row['subjects'])</code> if you need an array back.</p>
<h2>FAQ</h2>
<h3>How do I use ORDER BY inside GROUP_CONCAT?</h3>
<p>Put it inside the brackets, after the column: <code>GROUP_CONCAT(subject ORDER BY subject ASC)</code>. It sorts the values within each concatenated string. The query’s outer <code>ORDER BY</code> sorts the result rows separately.</p>
<h3>How do I change the GROUP_CONCAT separator?</h3>
<p>Add <code>SEPARATOR</code> as the last part inside the brackets: <code>GROUP_CONCAT(subject SEPARATOR ', ')</code>. The default is a comma with no space. Use <code>SEPARATOR ''</code> for no separator or <code>SEPARATOR '\n'</code> for one value per line.</p>
<h3>Why is my GROUP_CONCAT result cut off?</h3>
<p>It has hit <code>group_concat_max_len</code>, which was 1,024 bytes by default on my MySQL 8.4 server. MySQL truncates the string and raises warning 1260. Run <code>SET SESSION group_concat_max_len = 1000000;</code> before the query.</p>
<h3>Why does GROUP_CONCAT return NULL?</h3>
<p>Every value in that group was NULL, and aggregate functions ignore NULLs. Wrap the call in <code>COALESCE(GROUP_CONCAT(col), '')</code> to get an empty string or a default label instead.</p>
<h3>How do I remove duplicates in GROUP_CONCAT?</h3>
<p>Add <code>DISTINCT</code> straight after the opening bracket: <code>GROUP_CONCAT(DISTINCT subject ORDER BY subject SEPARATOR ', ')</code>.</p>
<h3>Can I limit the number of values in GROUP_CONCAT?</h3>
<p>Not with a <code>LIMIT</code> inside the function in MySQL, but you can order the values and keep the first N with <code>SUBSTRING_INDEX(GROUP_CONCAT(col ORDER BY score DESC), ',', N)</code>. For complex cases, use <code>ROW_NUMBER()</code> in a subquery.</p>
<h3>What is the difference between GROUP_CONCAT and JSON_ARRAYAGG?</h3>
<p><code>GROUP_CONCAT</code> returns a plain string with your chosen separator and supports <code>ORDER BY</code> and <code>DISTINCT</code> inside the call. <code>JSON_ARRAYAGG</code> returns a JSON array with proper quoting, which is safer to parse in code.</p>
<hr />
<p><em>Originally published on <a href="https://mfaysal.com/blog/mysql-group-concat-order-by-separator">mfaysal.com</a>.</em></p>
<p>I'm learning cybersecurity on TryHackMe — follow my progress at <a href="https://tryhackme.com/p/mfaysal.dev">tryhackme.com/p/mfaysal.dev</a>.</p>
]]></content:encoded></item><item><title><![CDATA[What Is %windir%? Windows Environment Variables Explained (With a List)]]></title><description><![CDATA[%windir% is a Windows environment variable that holds the path of the Windows folder on the system drive, which is usually C:\Windows. The percent signs tell Windows to swap the name for its value, so]]></description><link>https://mfaysaldev.hashnode.dev/what-is-windir-windows-environment-variables-explained-with-a-list</link><guid isPermaLink="true">https://mfaysaldev.hashnode.dev/what-is-windir-windows-environment-variables-explained-with-a-list</guid><category><![CDATA[cybersecurity]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Windows]]></category><dc:creator><![CDATA[Mahir Faysal]]></dc:creator><pubDate>Wed, 14 Oct 2026 04:04:08 GMT</pubDate><content:encoded><![CDATA[<p><strong><code>%windir%</code> is a Windows environment variable that holds the path of the Windows folder on the system drive, which is usually <code>C:\Windows</code>.</strong> The percent signs tell Windows to swap the name for its value, so typing <code>%windir%\System32</code> in the Run box opens <code>C:\Windows\System32</code>. <code>%SystemRoot%</code> points to the same folder. Environment variables like these let scripts, installers and tutorials refer to folders without hard-coding a drive letter or user name.</p>
<p>I kept seeing <code>%windir%</code>, <code>%appdata%</code> and <code>%temp%</code> in Windows security labs and setup guides without really knowing what they were, so I worked through Microsoft’s documentation (checked on 11 October 2026) and wrote down what actually matters. This post covers what the common variables mean, where they come from and how to view or change them safely.</p>
<h2>What is an environment variable?</h2>
<p>An <strong>environment variable</strong> is a named piece of text that Windows hands to every program it starts. Each process gets an <strong>environment block</strong>, a list of <code>NAME=value</code> pairs. Microsoft’s <a href="https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables">environment variables documentation</a> explains that there are two types, <strong>user</strong> variables (set for each user) and <strong>system</strong> variables (set for everyone), and that a child process inherits the environment of its parent by default.</p>
<p>That inheritance is the key idea. When you open Command Prompt from the Start menu, it inherits the environment from Explorer. Any program you start from that Command Prompt inherits it from there. That’s why changing a variable in one window doesn’t magically change it in programs that are already running.</p>
<p>A few more rules from the same page:</p>
<ul>
<li>A variable name can’t contain an equals sign (<code>=</code>).</li>
<li>A user-defined variable can be up to 32,767 characters long.</li>
<li>Names aren’t case-sensitive on Windows, so <code>%WINDIR%</code>, <code>%windir%</code> and <code>%WinDir%</code> are the same thing.</li>
</ul>
<h2>What %windir% and %SystemRoot% mean</h2>
<p>Microsoft’s <a href="https://learn.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables">list of recognised environment variables</a> describes <code>WINDIR</code> as the variable that “refers to the Windows folder located on the system drive” and lists <code>SYSTEMROOT</code> as “same as WINDIR”. Its typical value is <code>C:\Windows</code>.</p>
<p>So why do both exist? Historically, <code>%SystemRoot%</code> belongs to the Windows NT family, while <code>%windir%</code> was already used by the older DOS-based versions of Windows, and both were kept for compatibility. Today they hold the same path on a normal installation, and you’ll see both in Microsoft’s own documentation and registry values. In my experience, <code>%SystemRoot%</code> turns up more in registry paths and service settings, and <code>%windir%</code> more in scripts and Run-box shortcuts.</p>
<p>Useful folders under <code>%windir%</code>:</p>
<ul>
<li><strong><code>%windir%\System32</code></strong>: What’s there: Core 64-bit system programs and libraries (<code>cmd.exe</code>, <code>notepad.exe</code>, most <code>.msc</code> consoles).</li>
<li><strong><code>%windir%\SysWOW64</code></strong>: What’s there: 32-bit system files on 64-bit Windows.</li>
<li><strong><code>%windir%\System32\drivers\etc</code></strong>: What’s there: The <code>hosts</code> file, which maps names to IP addresses.</li>
<li><strong><code>%windir%\Temp</code></strong>: What’s there: Temporary files used by the system and services.</li>
<li><strong><code>%windir%\Fonts</code></strong>: What’s there: Installed fonts.</li>
<li><strong><code>%windir%\Logs</code></strong>: What’s there: Various Windows log files.</li>
</ul>
<p>That <code>hosts</code> file comes up a lot in security courses, because malware has historically edited it to redirect websites. It’s protected: you need admin rights to change anything under <code>%windir%</code>.</p>
<h2>Common Windows environment variables (list)</h2>
<p>These are the variables you’ll meet most often. Typical values are from Microsoft’s recognised variables list and assume Windows is on <code>C:</code> and the user is called <code>mahir</code>.</p>
<ul>
<li><strong><code>%windir%</code> / <code>%SystemRoot%</code></strong>: <code>C:\Windows</code> (system variable).</li>
<li><strong><code>%SystemDrive%</code></strong>: <code>C:</code> (a drive, not a folder: no trailing backslash) (system variable).</li>
<li><strong><code>%ProgramFiles%</code></strong>: <code>C:\Program Files</code> (system variable).</li>
<li><strong><code>%ProgramFiles(x86)%</code></strong>: <code>C:\Program Files (x86)</code> on 64-bit Windows (system variable).</li>
<li><strong><code>%ProgramData%</code></strong>: <code>C:\ProgramData</code> (application data for all users) (system variable).</li>
<li><strong><code>%ALLUSERSPROFILE%</code></strong>: <code>C:\ProgramData</code> (system variable).</li>
<li><strong><code>%USERPROFILE%</code></strong>: <code>C:\Users\mahir</code> (user variable).</li>
<li><strong><code>%APPDATA%</code></strong>: <code>C:\Users\mahir\AppData\Roaming</code> (user variable).</li>
<li><strong><code>%LOCALAPPDATA%</code></strong>: <code>C:\Users\mahir\AppData\Local</code> (user variable).</li>
<li><strong><code>%TEMP%</code> / <code>%TMP%</code></strong>: <code>C:\Users\mahir\AppData\Local\Temp</code> (user variable).</li>
<li><strong><code>%USERNAME%</code></strong>: <code>mahir</code> (user variable).</li>
<li><strong><code>%COMPUTERNAME%</code></strong>: The PC’s name (system variable).</li>
<li><strong><code>%PATH%</code></strong>: A semicolon-separated list of folders (user and system values combined).</li>
<li><strong><code>%COMSPEC%</code></strong>: <code>C:\Windows\System32\cmd.exe</code> (system variable).</li>
</ul>
<p><strong>Roaming vs Local AppData:</strong> Microsoft describes <code>%APPDATA%</code> (Roaming) as a common repository for application-specific data, which can roam with a user profile on managed networks, and <code>%LOCALAPPDATA%</code> as the home for local, non-roaming application data. Browser caches and large files usually sit in Local; small settings files often sit in Roaming.</p>
<h2>How to use environment variables in Explorer and the Run box</h2>
<p>You can type a variable anywhere Windows accepts a path:</p>
<ol>
<li>Press <strong>Win + R</strong> to open Run.</li>
<li>Type <code>%temp%</code> and press <strong>Enter</strong>. Your temporary files folder opens.</li>
<li>Try <code>%appdata%</code>, <code>%userprofile%\Downloads</code> or <code>%windir%\System32\drivers\etc</code>.</li>
</ol>
<p>Explorer’s address bar works the same way. This is handy when a guide says “delete the app’s folder in AppData”: you don’t have to un-hide anything or know the user name, you just type <code>%appdata%</code>.</p>
<p>Clearing <code>%temp%</code> is a classic safe tidy-up: close your programs, open <code>%temp%</code>, select everything and delete it. Windows will skip any file that’s still in use.</p>
<h2>Viewing environment variables in Command Prompt</h2>
<p>In Command Prompt (<code>cmd.exe</code>) you read a variable by wrapping its name in percent signs:</p>
<pre><code class="language-text">C:\&gt; echo %windir%
C:\Windows

C:\&gt; echo %USERPROFILE%
C:\Users\mahir
</code></pre>
<p>To see everything, use the built-in <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1">set command</a>. Microsoft notes that with no parameters it “displays the current environment variable settings”, and that <code>set</code> with part of a name lists every variable that begins with it:</p>
<pre><code class="language-text">C:\&gt; set
C:\&gt; set prog
ProgramData=C:\ProgramData
ProgramFiles=C:\Program Files
ProgramFiles(x86)=C:\Program Files (x86)
</code></pre>
<p>The output above is the typical shape; exact values depend on your PC.</p>
<p>One quirk worth knowing: if a variable doesn’t exist, Command Prompt doesn’t show an error. <code>echo %NOSUCHVAR%</code> simply prints <code>%NOSUCHVAR%</code> back at you, percent signs and all. So if you see the raw name in a script’s output, that’s your clue the variable is misspelt or was never set in that window. In a batch file, <code>if defined NOSUCHVAR echo yes</code> is the tidy way to test for it before you use it.</p>
<h2>Temporary vs permanent variables: set vs setx</h2>
<p>This is where most beginners get caught out.</p>
<p><strong><code>set</code> is temporary.</strong> <code>set MYVAR=hello</code> creates or changes a variable for the current Command Prompt window and the programs started from it. Close the window and it’s gone. Microsoft’s user environment variables page says the same thing: variables created with <code>set</code> “apply only to the command window in which they are set, and to its child processes”.</p>
<p><strong><code>setx</code> is permanent.</strong> Microsoft’s <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/setx">setx documentation</a> says it writes variables “to the master environment in the registry”. By default it sets a user variable; add <code>/m</code> to set a system variable (which needs an elevated prompt).</p>
<pre><code class="language-text">setx MYVAR "hello"
setx MYVAR "hello" /m
</code></pre>
<p>Three <code>setx</code> gotchas, all straight from the docs:</p>
<ol>
<li><strong>It doesn’t affect the current window.</strong> “Variables set with setx variables are available in future command windows only, not in the current command window.” Open a new window to see the change.</li>
<li><strong>It truncates at 1,024 characters.</strong> If you assign more, the content is cropped, and if you were overwriting an existing variable you can lose data.</li>
<li><strong>It expands references.</strong> If <code>%PATH%</code> contains <code>%JAVADIR%</code> and you rewrite <code>PATH</code> with <code>setx</code>, the reference is replaced with its current value, so later changes to <code>JAVADIR</code> won’t flow through.</li>
</ol>
<p>Point 2 and point 3 are why you should <strong>never</strong> run <code>setx PATH "%PATH%;C:\something"</code>. On many PCs, <code>PATH</code> is already near or over 1,024 characters, and that command merges the user and system PATH into your user variable while cropping it. Use the graphical editor below instead.</p>
<h2>Environment variables in PowerShell: $Env:windir</h2>
<p>PowerShell doesn’t use percent signs. According to Microsoft’s <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables">about_Environment_Variables</a> page, you use the <code>$Env:</code> prefix:</p>
<pre><code class="language-powershell">$Env:windir
# C:\Windows

$Env:Foo = 'An example'   # current session only
Get-ChildItem Env:        # list every variable
</code></pre>
<p>Setting <code>$Env:Foo</code> only changes the current PowerShell session, just like <code>set</code> in Command Prompt. The same page notes that environment variable names are case-sensitive on macOS and Linux, unlike on Windows, so <code>$Env:Path</code> and <code>$Env:PATH</code> are different there. That difference bites people who write cross-platform scripts.</p>
<h2>How to edit environment variables in Windows 11 and 10</h2>
<p>For permanent changes, the graphical editor is the safest route:</p>
<ol>
<li>Press <strong>Win</strong>, type <strong>environment variables</strong> and choose <strong>Edit the system environment variables</strong> (or <strong>Edit environment variables for your account</strong> for user-only changes).</li>
<li>Click <strong>Environment Variables…</strong>.</li>
<li>The top list holds your <strong>user</strong> variables; the bottom list holds <strong>system</strong> variables.</li>
<li>Select a variable and click <strong>Edit</strong>. For <code>Path</code>, Windows shows one folder per line, so you can add, remove or reorder entries without editing one long string.</li>
<li>Click <strong>OK</strong> in every window, then restart any program that needs the new value.</li>
</ol>
<p>Behind the scenes, Microsoft documents that system variables live in the registry under <code>HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment</code>, and that programs which change them should broadcast a <code>WM_SETTINGCHANGE</code> message so the shell picks up the update. User variables live under <code>HKEY_CURRENT_USER\Environment</code>.</p>
<h2>How PATH works (and why order matters)</h2>
<p><code>PATH</code> is the most important variable for anyone who codes. When you type <code>python</code> or <code>git</code> without a full path, Windows searches the current folder and then each folder listed in <code>PATH</code>, in order, for a matching program. The first match wins.</p>
<p>That has two consequences:</p>
<ul>
<li><strong>“Not recognised as an internal or external command”</strong> usually means the program’s folder isn’t in <code>PATH</code>, or you haven’t opened a new terminal since adding it.</li>
<li><strong>Order matters for security.</strong> If an attacker can write to a folder that appears early in <code>PATH</code>, they can drop a fake <code>python.exe</code> or <code>git.exe</code> there and it will run instead of the real one. Keep user-writable folders out of the system <code>PATH</code>, and check new entries before adding them.</li>
</ul>
<p>You can see which copy of a program wins with <code>where</code>:</p>
<pre><code class="language-text">C:\&gt; where python
</code></pre>
<h2>Environment variables and security: what to look for</h2>
<p>Once you know these variables, a lot of security advice makes more sense. Here’s why they keep appearing in labs and incident write-ups.</p>
<p><strong>User-writable folders are where unwanted programs hide.</strong> <code>%TEMP%</code>, <code>%APPDATA%</code> and <code>%LOCALAPPDATA%</code> all sit inside your user profile, so any program you run can write there without an admin prompt. That makes them convenient for legitimate apps and for malware alike. When you’re checking a PC for something suspicious, look for unfamiliar <code>.exe</code> files in these folders, especially ones set to start automatically.</p>
<p><strong>Protected folders need elevation.</strong> <code>%windir%</code> and <code>%ProgramFiles%</code> are writable only with administrator rights. A program that installs itself there had to get past a UAC prompt (I explain those in <a href="https://mfaysal.com/blog/what-is-uac-in-windows">what UAC in Windows is</a>), which is a useful clue when you’re working out how something arrived.</p>
<p><strong>Variables are visible to every process you run.</strong> Any program started under your account can read your user and system environment variables. They’re fine for configuration, and better than hard-coding secrets in source files, but they aren’t a vault. On a shared PC, don’t store passwords in system variables, and on a server, give each app only the keys it needs.</p>
<p><strong>Scripts should use variables, not hard-coded paths.</strong> A script that writes to <code>C:\Users\mahir\Desktop</code> breaks for every other user and on any PC where Windows isn’t on <code>C:</code>. Using <code>%USERPROFILE%\Desktop</code> or <code>%SystemDrive%</code> keeps it portable, and it’s easier to review, because the intent is obvious.</p>
<h2>Reading environment variables in code</h2>
<p>Environment variables are also how developers keep configuration and secrets out of source code. The same idea runs from a Windows PC up to a cloud host:</p>
<pre><code class="language-python">import os
print(os.environ.get("WINDIR"))      # C:\Windows on Windows, None on Linux
print(os.environ.get("APPDATA", "not set"))
</code></pre>
<pre><code class="language-php">&lt;?php
echo getenv('windir') ?: 'not on Windows';
</code></pre>
<p>Using <code>.get()</code> with a default in Python, or checking <code>getenv()</code>’s <code>false</code> return in PHP, means your script won’t crash on a machine where the variable doesn’t exist. Hosting platforms use the same mechanism for API keys; I used it when <a href="https://mfaysal.com/blog/gemini-api-key-nextjs-vercel">setting a Gemini API key on Vercel</a>, so the key never lands in the code repository.</p>
<h2>FAQ</h2>
<h3>What is %windir% in Windows?</h3>
<p><code>%windir%</code> is an environment variable that stores the path of the Windows folder on the system drive, usually <code>C:\Windows</code>. Windows replaces the variable with that path when you use it in the Run box, Explorer, a script or a shortcut.</p>
<h3>What is the difference between %windir% and %SystemRoot%?</h3>
<p>On a normal installation there’s no difference in value: Microsoft lists <code>SYSTEMROOT</code> as “same as WINDIR”, and both point to the Windows folder. <code>%SystemRoot%</code> comes from the Windows NT line and is common in registry values, while <code>%windir%</code> is older and common in scripts.</p>
<h3>Where is %appdata%?</h3>
<p><code>%appdata%</code> points to the Roaming application data folder, typically <code>C:\Users\&lt;username&gt;\AppData\Roaming</code>. <code>%localappdata%</code> points to <code>C:\Users\&lt;username&gt;\AppData\Local</code>. Type either into the Run box to open it.</p>
<h3>Is it safe to delete files in %temp%?</h3>
<p>Generally yes. <code>%temp%</code> holds temporary files, and Windows won’t let you delete files that are still in use. Close your programs first, then delete what’s left. Don’t delete the Temp folder itself.</p>
<h3>What is the difference between set and setx?</h3>
<p><code>set</code> changes a variable only in the current Command Prompt window and its child processes. <code>setx</code> saves the variable permanently in the registry, but the change only appears in new windows, and values longer than 1,024 characters are cropped.</p>
<h3>How do I see all environment variables in Windows?</h3>
<p>Run <code>set</code> in Command Prompt or <code>Get-ChildItem Env:</code> in PowerShell. For permanent user and system variables, open <strong>Edit the system environment variables</strong> and click <strong>Environment Variables…</strong>.</p>
<h3>Why does my new PATH entry not work?</h3>
<p>Programs read their environment when they start, so close and reopen your terminal or editor after editing <code>PATH</code>. Also check the folder you added actually contains the program, and that there isn’t a typo or a stray quote in the entry.</p>
<hr />
<p><em>Originally published on <a href="https://mfaysal.com/blog/windows-environment-variables-windir">mfaysal.com</a>.</em></p>
<p>I'm learning cybersecurity on TryHackMe — follow my progress at <a href="https://tryhackme.com/p/mfaysal.dev">tryhackme.com/p/mfaysal.dev</a>.</p>
]]></content:encoded></item><item><title><![CDATA[What Is UAC in Windows? User Account Control Explained for Beginners]]></title><description><![CDATA[UAC (User Account Control) is the Windows security feature that makes even administrator accounts run programs with standard user rights, and asks you to approve or supply a password before anything g]]></description><link>https://mfaysaldev.hashnode.dev/what-is-uac-in-windows-user-account-control-explained-for-beginners</link><guid isPermaLink="true">https://mfaysaldev.hashnode.dev/what-is-uac-in-windows-user-account-control-explained-for-beginners</guid><category><![CDATA[Windows]]></category><category><![CDATA[cybersecurity]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Mahir Faysal]]></dc:creator><pubDate>Mon, 12 Oct 2026 04:05:38 GMT</pubDate><content:encoded><![CDATA[<p><strong>UAC (User Account Control) is the Windows security feature that makes even administrator accounts run programs with standard user rights, and asks you to approve or supply a password before anything gets full admin power.</strong> Thatâ€™s the â€œDo you want to allow this app to make changes to your device?â€ box. It exists to stop malware from silently getting administrator privileges, and Microsoft ships it switched on by default.</p>
<p>Iâ€™m learning Windows security alongside web development, and UAC was one of the first things that confused me. Why does an admin account still get asked? Why does the whole screen go dark? Is turning it off a sensible shortcut? This guide answers those questions using Microsoftâ€™s own documentation, checked on 11 October 2026, and avoids the scary jargon where possible.</p>
<h2>What UAC actually does</h2>
<p>Microsoft describes UAC as a feature that â€œprotects the operating system from unauthorized changesâ€. When a change needs administrator permission, UAC notifies you and gives you the chance to approve or deny it. The important idea is <strong>least privilege</strong>: programs only get the rights they need, and only when you agree.</p>
<p>Without UAC, any program you start from an admin account would inherit full control of the machine. A dodgy download, a malicious macro or a compromised browser extension could install drivers, change system files or disable your antivirus without asking. UAC puts a speed bump in that path. Malware still has to trick you into clicking <strong>Yes</strong>, and thatâ€™s a much harder job than running quietly in the background.</p>
<p>UAC also protects itself. Microsoft notes that, unless you disable UAC, malicious software is prevented from disabling or interfering with UAC settings. You need admin rights to change how it behaves.</p>
<h2>How UAC works: one account, two access tokens</h2>
<p>When you sign in, Windows creates an <strong>access token</strong>: a small bundle of data that says who you are, which groups you belong to and which privileges you hold. Every program you start carries a copy of that token, and Windows checks it whenever the program tries to open a file, change a setting or talk to another process.</p>
<p>Hereâ€™s the clever bit. According to <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/how-it-works">Microsoftâ€™s explanation of how UAC works</a>, when an administrator signs in, Windows creates <strong>two</strong> tokens:</p>
<ul>
<li>A <strong>standard user access token</strong>, with the administrative privileges and group SIDs removed.</li>
<li>An <strong>administrator access token</strong>, with everything.</li>
</ul>
<p>Windows uses the standard token to start <code>explorer.exe</code>, the desktop shell. Because almost every program you launch is a child of Explorer, they all inherit the standard token too. So your browser, your email client and that random installer all start with ordinary rights, even though your account is technically an admin.</p>
<p>Only when a program asks for admin rights does UAC step in, show a prompt, and (if you agree) start that one program with the full administrator token. Microsoft calls this setup <strong>Admin Approval Mode</strong>.</p>
<h3>Integrity levels</h3>
<p>Windows also tags each process with an <strong>integrity level</strong>, a measure of trust. A disk partitioning tool that changes system data runs at high integrity. A web browser, which handles untrusted content all day, runs at a lower level. A process with a lower integrity level canâ€™t modify a process with a higher one. Thatâ€™s one reason a compromised browser tab canâ€™t simply reach into an elevated admin tool.</p>
<h2>The consent prompt vs the credential prompt</h2>
<p>Youâ€™ll see one of two prompts, depending on what kind of account youâ€™re using.</p>
<ul>
<li><strong>Who sees it</strong>: Consent prompt: Members of the Administrators group; Credential prompt: Standard users (by default).</li>
<li><strong>What it asks</strong>: Consent prompt: Click <strong>Yes</strong> or <strong>No</strong>; Credential prompt: Type an administratorâ€™s user name and password.</li>
<li><strong>What happens on Yes</strong>: Consent prompt: The app runs with your admin token; Credential prompt: The app runs with that admin accountâ€™s token.</li>
</ul>
<p>The <strong>consent prompt</strong> is what most home users know, because the first account on a new PC is usually an administrator. The <strong>credential prompt</strong> appears when youâ€™re signed in as a standard user and something needs admin rights. In that case, typing your own password wonâ€™t help; you need the details of an account thatâ€™s in the Administrators group.</p>
<p>Microsoftâ€™s own recommendation is that â€œyour primary user account is a standard userâ€. Running day to day as a standard user means a malicious program canâ€™t even get a Yes-click elevation from you; it needs a password youâ€™d have to type deliberately.</p>
<h2>Why the UAC prompt is grey or yellow</h2>
<p>The colour of the banner tells you something about the program asking for permission. Before showing the prompt, Windows checks the executableâ€™s digital signature:</p>
<ul>
<li><strong>Grey banner:</strong> a Windows administrative app, such as a Control Panel item, or a program signed by a verified publisher.</li>
<li><strong>Yellow banner:</strong> the program is unsigned, or itâ€™s signed but not trusted.</li>
</ul>
<p>A yellow prompt isnâ€™t automatically evil. Plenty of small, legitimate tools are unsigned. But it is a cue to slow down. Ask yourself: did I just start this? Do I know where it came from? Is the program name what I expected? If a yellow prompt appears out of nowhere while youâ€™re browsing, click <strong>No</strong>.</p>
<h3>The shield icon</h3>
<p>Youâ€™ve probably seen a small blue-and-yellow shield on certain buttons in Windows. It marks an action that needs admin rights. Microsoftâ€™s example is the Date and Time settings: a standard user can change the time zone freely, but changing the system clock itself requires a full administrator token, so that button carries the shield.</p>
<h2>The secure desktop: why your screen goes dark</h2>
<p>When a UAC prompt appears, the rest of the screen dims and you canâ€™t click anything else. That isnâ€™t just a visual effect. Windows has switched to the <strong>secure desktop</strong>, a separate desktop that only Windows processes can access.</p>
<p>The point is to stop other programs from tampering with the prompt. On the normal (interactive) desktop, a malicious program could try to send fake mouse clicks to the <strong>Yes</strong> button, or draw something over the prompt to mislead you. On the secure desktop, ordinary programs simply canâ€™t reach the prompt. Microsoft recommends keeping the <strong>Switch to the secure desktop when prompting for elevation</strong> policy enabled, and itâ€™s enabled by default. Since Windows Server 2019, you also canâ€™t paste clipboard content onto the secure desktop, and the same applies to supported Windows client versions.</p>
<p>Malware can draw an imitation of the dimmed screen, but Microsoft points out that with the default consent behaviour it doesnâ€™t gain elevation even if you click Yes on the fake. The real risk is a fake <em>credential</em> prompt that harvests a password, which is another reason to only type admin passwords when you started the action yourself.</p>
<h2>The four UAC slider levels</h2>
<p>To see your current setting, open the Start menu, type <strong>UAC</strong> and choose <strong>Change User Account Control settings</strong>. Youâ€™ll see a slider with four positions. The descriptions below follow Microsoftâ€™s <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/architecture">UAC architecture documentation</a>.</p>
<ul>
<li><strong>Always notify:</strong> prompts when apps make changes and when you change Windows settings; dims the desktop.</li>
<li><strong>Notify me only when apps try to make changes (default):</strong> prompts for apps but not for your own settings changes; dims the desktop.</li>
<li><strong>Notify me only when apps try to make changes (do not dim my desktop):</strong> same prompts as the default, but no dimming.</li>
<li><strong>Never notify:</strong> no prompts and no dimming.</li>
</ul>
<p><strong>Always notify</strong> is the strictest level. Microsoft suggests it if you often install new software or visit unfamiliar websites. Youâ€™ll see more prompts, including when you open built-in admin tools.</p>
<p><strong>The default level</strong> prompts when programs try to install software or change your computer, but not when you change Windows settings yourself. It still uses the secure desktop. For most people, this is the right balance.</p>
<p><strong>The â€œdo not dimâ€ level</strong> shows the prompt on your normal desktop. Microsoft doesnâ€™t recommend it and says to choose it only if dimming takes a long time on your computer. You lose the secure desktop protection described above.</p>
<p><strong>Never notify</strong> doesnâ€™t fully switch UAC off, which surprises many people. The UAC service keeps running, elevation requests from administrators are approved automatically with no prompt, and elevation requests from standard users are automatically denied. Microsoft labels this option â€œnot recommended due to security concernsâ€.</p>
<h2>Where UAC settings live: policies and the registry</h2>
<p>The slider is a friendly front end for a set of security policies. On Pro, Enterprise and Education editions you can see them in the Local Security Policy console (<code>secpol.msc</code>) under <strong>Security Settings â€º Local Policies â€º Security Options</strong>; every one starts with â€œUser Account Control:â€. Behind those policies are registry values under this key:</p>
<pre><code class="language-text">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
</code></pre>
<p>These are the values beginners most often run into, with the defaults taken from <a href="https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/settings-and-configuration">Microsoftâ€™s UAC settings reference</a>:</p>
<ul>
<li><strong><code>EnableLUA</code></strong> (default 1, enabled): Run all administrators in Admin Approval Mode, the master switch.</li>
<li><strong><code>ConsentPromptBehaviorAdmin</code></strong> (default 5): What admins see: 0 = elevate without prompting, 1 = credentials on secure desktop, 2 = consent on secure desktop, 3 = credentials, 4 = consent, 5 = consent for non-Windows binaries.</li>
<li><strong><code>ConsentPromptBehaviorUser</code></strong> (default 3): What standard users see: 0 = automatically deny, 1 = credentials on secure desktop, 3 = credentials.</li>
<li><strong><code>PromptOnSecureDesktop</code></strong> (default 1, enabled): Show prompts on the secure desktop.</li>
<li><strong><code>FilterAdministratorToken</code></strong> (default 0, disabled): Admin Approval Mode for the built-in Administrator account.</li>
<li><strong><code>EnableVirtualization</code></strong> (default 1, enabled): Redirect failed writes from old apps to per-user locations.</li>
</ul>
<p>You can read these safely in PowerShell without changing anything:</p>
<pre><code class="language-powershell">Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' |
  Select-Object EnableLUA, ConsentPromptBehaviorAdmin, ConsentPromptBehaviorUser, PromptOnSecureDesktop
</code></pre>
<p>Notice that the default for admins, value <strong>5</strong>, means â€œprompt for consent for non-Windows binariesâ€. Thatâ€™s why some built-in Windows tools open without a prompt at the default slider level, while third-party installers always ask.</p>
<h3>The built-in Administrator account is different</h3>
<p>Windows has a hidden account literally called <strong>Administrator</strong>. By default, <code>FilterAdministratorToken</code> is 0, which means that account does <strong>not</strong> use Admin Approval Mode: it runs every program with full admin rights and no prompts. Itâ€™s disabled on normal installs for good reason. If a tutorial tells you to enable it and use it as your daily account, donâ€™t.</p>
<h2>File and registry virtualisation</h2>
<p>Some older programs assume they could write to protected folders such as <code>C:\Program Files</code> or <code>C:\Windows</code>. Under UAC those writes would fail. To keep such apps working, Windows can quietly redirect the failed writes to per-user locations instead. Microsoft lists the protected locations as <code>%ProgramFiles%</code>, <code>%Windir%</code>, <code>%Windir%\system32</code> and <code>HKLM\Software</code>.</p>
<p>This is the <strong>Virtualize file and registry write failures to per-user locations</strong> policy, and itâ€™s on by default. If youâ€™ve ever â€œsavedâ€ a settings file into Program Files and then couldnâ€™t find it later, virtualisation is a likely reason. If those <code>%Windir%</code> style names look unfamiliar, theyâ€™re environment variables, a topic Iâ€™ll cover separately.</p>
<h2>Should you turn UAC off?</h2>
<p>No. Itâ€™s tempting when an old game or tool keeps prompting, but the trade-off is poor:</p>
<ul>
<li>With <code>EnableLUA</code> disabled, every program you start from an admin account runs with full admin rights, which is exactly the situation UAC was built to prevent.</li>
<li>Microsoft notes that if you disable Admin Approval Mode, Windows Security warns you that the overall security of the operating system is reduced.</li>
<li>Microsoft also warns that some Universal Windows Platform apps might not work when UAC is fully disabled.</li>
</ul>
<p>Better fixes for annoying prompts:</p>
<ol>
<li><strong>Right-click â€º Run as administrator</strong> only for the specific tool that needs it.</li>
<li>Update or replace old software that writes to protected folders.</li>
<li>Use a standard account day to day and keep a separate admin account for installs. The extra password step makes you think before granting power.</li>
<li>If youâ€™re a developer, install tools per-user (many installers offer a â€œjust for meâ€ option) so they donâ€™t need elevation.</li>
</ol>
<p>The same least-privilege thinking applies when you build software: give each part only the access it needs, and handle credentials carefully, for example by <a href="https://mfaysal.com/blog/php-password-hash-verify-example">hashing passwords properly in PHP</a> rather than storing them as plain text.</p>
<h2>UAC is not a security boundary (and why that matters)</h2>
<p>If you start reading about Windows security, youâ€™ll come across the term <strong>UAC bypass</strong>: a technique that gets elevated rights without showing a prompt. In Microsoftâ€™s <a href="https://www.microsoft.com/en-us/msrc/windows-security-servicing-criteria">Windows Security Servicing Criteria</a>, UAC is listed as a security <em>feature</em>, not a security <em>boundary</em>, and Microsoft doesnâ€™t commit to fixing UAC bypasses as security vulnerabilities by default. Thatâ€™s another strong argument for running as a standard user: thereâ€™s nothing to bypass from a standard account if the attacker doesnâ€™t know an admin password.</p>
<p>For a beginner, the practical lesson is simple. UAC reduces risk a lot, but it relies on you. If you click <strong>Yes</strong> to everything, it canâ€™t help. Read the program name and publisher, and if you didnâ€™t start the action, say no.</p>
<h2>How to check UAC is working on your PC</h2>
<p>A quick health check you can do in under a minute:</p>
<ol>
<li>Open <strong>Change User Account Control settings</strong> and confirm the slider is on the default level or higher.</li>
<li>Right-click <strong>Command Prompt</strong> or <strong>Terminal</strong> and choose <strong>Run as administrator</strong>. You should see a prompt on a dimmed screen.</li>
<li>Run <code>whoami /groups</code> in a normal (non-elevated) window and look for a line containing <code>Mandatory Label\Medium Mandatory Level</code>. In the elevated window, the same command shows <code>High Mandatory Level</code>. That difference is UACâ€™s split token in action.</li>
<li>Open <strong>Settings â€º Accounts â€º Your info</strong> and check whether your everyday account says <strong>Administrator</strong>. If it does, consider creating a standard account for daily use.</li>
</ol>
<h2>FAQ</h2>
<h3>What does UAC stand for in Windows?</h3>
<p>UAC stands for User Account Control. Itâ€™s the Windows feature that runs programs with standard user rights and asks for consent or an administrator password before a program gets full admin privileges.</p>
<h3>Why does Windows ask for permission when Iâ€™m an administrator?</h3>
<p>Because an administrator account gets two access tokens at sign-in. Windows runs your programs with the restricted standard token and only uses the full admin token after you approve a UAC prompt. This is called Admin Approval Mode.</p>
<h3>Why does my screen go dark when a UAC prompt appears?</h3>
<p>Windows switches to the secure desktop, which only Windows processes can access. Other programs canâ€™t click or draw over the prompt, so malware canâ€™t approve the request for you.</p>
<h3>Is it safe to set UAC to Never notify?</h3>
<p>No. Microsoft says this level isnâ€™t recommended because of security concerns. Programs started by administrators get elevated automatically without asking, and standard usersâ€™ elevation requests are silently denied.</p>
<h3>What is the difference between a yellow and a grey UAC prompt?</h3>
<p>A grey prompt means the program is a Windows administrative app or is signed by a verified publisher. A yellow prompt means the program is unsigned or its signature isnâ€™t trusted, so take extra care before clicking Yes.</p>
<h3>How do I fully disable UAC?</h3>
<p>The slider canâ€™t do it; youâ€™d have to disable the â€œRun all administrators in Admin Approval Modeâ€ policy (the <code>EnableLUA</code> registry value). It isnâ€™t recommended: it removes a key protection and can stop some Windows apps working.</p>
<h3>Does UAC protect a standard user account?</h3>
<p>Yes. A standard user sees a credential prompt and needs an administratorâ€™s user name and password to elevate. Microsoft recommends using a standard account as your main account for this reason.</p>
<hr />
<p><em>Originally published on <a href="https://mfaysal.com/blog/what-is-uac-in-windows">mfaysal.com</a>.</em></p>
<p>I'm learning cybersecurity on TryHackMe â€” follow my progress at <a href="https://tryhackme.com/p/mfaysal.dev">tryhackme.com/p/mfaysal.dev</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Python List Comprehension Examples: From Basics to Nested Loops]]></title><description><![CDATA[A Python list comprehension builds a new list in one line by writing [expression for item in iterable], with an optional if at the end to filter items. It replaces the "make an empty list, loop, appen]]></description><link>https://mfaysaldev.hashnode.dev/python-list-comprehension-examples-from-basics-to-nested-loops</link><guid isPermaLink="true">https://mfaysaldev.hashnode.dev/python-list-comprehension-examples-from-basics-to-nested-loops</guid><category><![CDATA[programming]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Mahir Faysal]]></dc:creator><pubDate>Wed, 07 Oct 2026 10:37:43 GMT</pubDate><content:encoded><![CDATA[<p>A Python list comprehension builds a new list in one line by writing <code>[expression for item in iterable]</code>, with an optional <code>if</code> at the end to filter items. It replaces the "make an empty list, loop, append" pattern. Below are the list comprehension examples I actually use, from the basic form to filtering, if/else, nested loops and the mistakes that tripped me up.</p>
<p>When I started learning Python, comprehensions looked like a puzzle. Once I learned to read them out loud, they became one of my favourite parts of the language. Every example here was run with Python 3.13, and the output in the comments at the bottom of each block is the real output.</p>
<h2>The basic syntax: from a for loop to one line</h2>
<p>Here is the same list of squares built two ways:</p>
<pre><code class="language-python"># The classic loop way
squares = []
for n in range(1, 6):
    squares.append(n * n)
print(squares)

# The same thing as a list comprehension
squares = [n * n for n in range(1, 6)]
print(squares)

# Output:
# [1, 4, 9, 16, 25]
# [1, 4, 9, 16, 25]
</code></pre>
<p>The comprehension has three parts inside square brackets:</p>
<ul>
<li><strong>The expression</strong> (<code>n * n</code>): what goes into the new list.</li>
<li><strong>The for clause</strong> (<code>for n in range(1, 6)</code>): where the items come from.</li>
<li><strong>An optional condition</strong> (<code>if ...</code>): which items to keep.</li>
</ul>
<p>The trick that helped me is to read it in plain English: "give me <code>n * n</code> for every <code>n</code> in 1 to 5." The result is always a brand new list. The original iterable is not changed.</p>
<h2>Filtering with if</h2>
<p>Put an <code>if</code> after the <code>for</code> clause and only the items that pass the test make it into the new list. This is the comprehension I write most often, usually to clean data before using it.</p>
<pre><code class="language-python">marks = [72, 45, 88, 39, 91, 60, 33]

# Keep only the passing marks (40 or more)
passed = [m for m in marks if m &gt;= 40]
print(passed)

# Clean up messy user input: strip spaces, drop empty strings
raw_tags = ["  python ", "", "html", "   ", " css"]
tags = [t.strip().lower() for t in raw_tags if t.strip()]
print(tags)

# Output:
# [72, 45, 88, 91, 60]
# ['python', 'html', 'css']
</code></pre>
<p>The second example does two jobs at once. The condition <code>if t.strip()</code> drops strings that are empty or only spaces, because an empty string counts as false. The expression <code>t.strip().lower()</code> then tidies up the ones that are left.</p>
<h2>Using if/else in a list comprehension</h2>
<p>This is the part that confused me the most. There are two different places a condition can go, and they do different things:</p>
<pre><code class="language-python">marks = [72, 45, 88, 39, 91]

# if/else goes BEFORE the for, because it decides the value
results = ["pass" if m &gt;= 40 else "fail" for m in marks]
print(results)

# A filter goes AFTER the for, because it decides whether to keep the item
high = [m for m in marks if m &gt;= 80]
print(high)

# Output:
# ['pass', 'pass', 'pass', 'fail', 'pass']
# [88, 91]
</code></pre>
<ul>
<li><strong><code>x if condition else y</code> before the <code>for</code></strong> is a conditional expression. It runs for every item and decides <em>what value</em> goes in. The new list is always the same length as the original.</li>
<li><strong><code>if condition</code> after the <code>for</code></strong> is a filter. It decides <em>whether</em> an item goes in at all, so the new list can be shorter.</li>
</ul>
<p>If you write <code>[m for m in marks if m &gt;= 40 else 0]</code>, Python raises a <code>SyntaxError</code>, because a filter can't have an <code>else</code>. Move the whole <code>if/else</code> to the front instead.</p>
<h2>List comprehension examples with real data</h2>
<p>Practice lists of numbers are fine, but most of the time I'm working with a list of dictionaries, such as rows from a CSV file or a JSON response from an API. Comprehensions are great for pulling out exactly the fields you need.</p>
<pre><code class="language-python">students = [
    {"name": "Rafi", "mark": 72},
    {"name": "Nusrat", "mark": 88},
    {"name": "Tanvir", "mark": 39},
]

names = [s["name"] for s in students]
print(names)

toppers = [s["name"] for s in students if s["mark"] &gt;= 70]
print(toppers)

# Pair two lists together with zip()
subjects = ["Math", "Physics", "Chemistry"]
scores = [81, 67, 74]
report = [f"{sub}: {score}" for sub, score in zip(subjects, scores)]
print(report)

# Output:
# ['Rafi', 'Nusrat', 'Tanvir']
# ['Rafi', 'Nusrat']
# ['Math: 81', 'Physics: 67', 'Chemistry: 74']
</code></pre>
<p>The <code>zip()</code> example shows that the <code>for</code> clause can unpack values, just like a normal loop. <code>for sub, score in zip(subjects, scores)</code> walks through both lists side by side. Mixing in an f-string in the expression is a common way to build labels or lines of a report.</p>
<h2>Nested list comprehensions</h2>
<p>You can use more than one <code>for</code> in a comprehension, and you can put a comprehension inside another one. These are powerful, but they are also where readability goes downhill fast.</p>
<pre><code class="language-python">grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Flatten: read the for clauses left to right, like nested loops
flat = [n for row in grid for n in row]
print(flat)

# Build a 3x4 table of zeros (a list of lists)
table = [[0 for col in range(4)] for row in range(3)]
print(table)

# Transpose rows into columns
columns = [[row[i] for row in grid] for i in range(3)]
print(columns)

# Output:
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
</code></pre>
<p>For the flatten example, read the <code>for</code> clauses in the same order you would write nested loops: first <code>for row in grid</code>, then <code>for n in row</code>. The <code>table</code> and <code>columns</code> examples are different: there the inner comprehension is the <em>expression</em>, so each item of the outer list is itself a new list.</p>
<blockquote>
<p>My rule: if I need more than two <code>for</code> clauses, or a nested comprehension plus a condition, I write a normal loop. Code is read more often than it is written.</p>
</blockquote>
<h2>Dict comprehensions, set comprehensions and generator expressions</h2>
<p>The same syntax works with other brackets. Curly braces with a <code>key: value</code> pair give you a dictionary, and curly braces with a single value give you a set. Round brackets give you a generator expression, which produces values one at a time instead of building a whole list in memory.</p>
<pre><code class="language-python">words = ["apple", "banana", "avocado", "cherry", "apple"]

# Dict comprehension: word -&gt; length
lengths = {w: len(w) for w in words}
print(lengths)

# Set comprehension: unique first letters
first_letters = {w[0] for w in words}
print(sorted(first_letters))

# Generator expression: no list is built, sum() pulls values one by one
total_chars = sum(len(w) for w in words)
print(total_chars)

# Output:
# {'apple': 5, 'banana': 6, 'avocado': 7, 'cherry': 6}
# ['a', 'b', 'c']
# 29
</code></pre>
<p>Notice that <code>"apple"</code> appears twice in the input but only once in the dictionary, because a later key overwrites an earlier one. I sorted the set before printing, since sets have no guaranteed order. When you only need to pass the values into a function like <code>sum()</code>, <code>max()</code>, <code>any()</code> or <code>"".join()</code>, a generator expression is enough, and you can drop the extra brackets.</p>
<h3>Bonus: the walrus operator inside a comprehension</h3>
<p>Sometimes the filter and the expression need the same calculated value. Since Python 3.8, the assignment expression <code>:=</code> lets you compute it once and reuse it:</p>
<pre><code class="language-python">readings = ["12", "x", "7", "", "30"]

def to_int(text):
    try:
        return int(text)
    except ValueError:
        return None

# := stores the converted value so to_int() runs only once per item
numbers = [n for r in readings if (n := to_int(r)) is not None]
print(numbers)

# Output:
# [12, 7, 30]
</code></pre>
<p>Without <code>:=</code>, you would call <code>to_int()</code> twice per item, once in the filter and once in the expression. It's a handy tool, but use it lightly, because it makes the line harder to read for beginners.</p>
<h2>Common mistakes with list comprehensions</h2>
<h3>1. Putting else in the wrong place</h3>
<p>As covered above, <code>if</code> alone goes at the end, while <code>if ... else ...</code> goes at the front. When you get a <code>SyntaxError</code> in a comprehension, check this first.</p>
<h3>2. Using a comprehension only for its side effects</h3>
<p>A comprehension is for building a list. If you only want to do something for each item, like printing or saving to a file, use a normal loop:</p>
<pre><code class="language-python">names = ["rafi", "nusrat"]

# Don't do this: a list of None values built only for the side effect
result = [print(n.title()) for n in names]
print(result)

# Do this instead
for n in names:
    print(n.title())

# Output:
# Rafi
# Nusrat
# [None, None]
# Rafi
# Nusrat
</code></pre>
<p>The first version works, but it also builds a useless list of <code>None</code> values, because <code>print()</code> returns <code>None</code>. Anyone reading it will wonder what <code>result</code> is for.</p>
<h3>3. Expecting the loop variable to exist afterwards</h3>
<p>In Python 3, a comprehension has its own scope, so its loop variable does not overwrite a variable with the same name outside it:</p>
<pre><code class="language-python">x = "outer"
values = [x for x in range(3)]
print(values)
print(x)  # the loop variable does not leak out in Python 3

# Output:
# [0, 1, 2]
# outer
</code></pre>
<p>This is a good thing, but it means you can't use the loop variable after the comprehension ends, the way you can after a normal <code>for</code> loop.</p>
<h3>4. Cramming too much into one line</h3>
<p>A comprehension that runs past the edge of the screen, with function calls, conditions and nested loops, is not clever. It's just hard to debug. Split long comprehensions over several lines inside the brackets, or switch to a loop.</p>
<h3>5. Building a big list you don't need</h3>
<p><code>sum([x * x for x in range(1_000_000)])</code> creates a million-item list first. <code>sum(x * x for x in range(1_000_000))</code> gives the same answer without storing the list.</p>
<h2>FAQ</h2>
<h3>Are list comprehensions faster than for loops?</h3>
<p>For simple cases they are usually a bit faster than a loop that calls <code>append()</code>, because there is less work per item. The difference is rarely what matters, though. Choose a comprehension because it's clearer for building a list, not to save microseconds.</p>
<h3>Can I use elif in a list comprehension?</h3>
<p>There is no <code>elif</code> keyword inside a comprehension, but you can chain conditional expressions: <code>["A" if m &gt;= 80 else "B" if m &gt;= 60 else "C" for m in marks]</code>. It works, but past two conditions I move the logic into a small function and call that function in the expression.</p>
<h3>What's the difference between a list comprehension and map()?</h3>
<p><code>map(func, items)</code> applies a function to every item and returns a lazy iterator. A list comprehension does the same job and can filter at the same time, and many Python developers find it easier to read. Both are fine. I mostly use comprehensions.</p>
<h3>Do list comprehensions work with strings and files?</h3>
<p>Yes. Any iterable works: strings give you characters, <code>range()</code> gives you numbers, and an open file gives you lines, so <code>[line.strip() for line in f]</code> is a common way to read a file into a clean list.</p>
<h2>Conclusion</h2>
<p>A list comprehension is a short way to say "build a new list from this iterable, transform each item, and optionally skip some". Start with the basic form, add a filter at the end when you need one, and remember that <code>if/else</code> goes at the front. When a comprehension gets hard to read, a plain loop is always allowed.</p>
<p>The official Python tutorial has a good section on <a href="https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions">list comprehensions</a> with more examples if you want to go deeper. I'm still learning too, and you can see what I've been building on my <a href="https://mfaysal.com/projects">projects page</a>.</p>
<p><em>Originally published at <a href="https://mfaysal.com/blog/python-list-comprehension-examples">mfaysal.com</a>.</em></p>
<p>More tutorials: <a href="https://mfaysal.com">https://mfaysal.com</a></p>
]]></content:encoded></item></channel></rss>