PONYλM2Modula-2

Visual Basic.CodeCompared.To/Python

An interactive executable cheatsheet comparing Visual Basic and Python

Visual Basic (.NET 10) Python 3.14
Output & Running
Hello, World
The smallest complete program in each language — the Python column is the whole file, not an excerpt.
Option Strict On Imports System Module HelloWorld Sub Main() Console.WriteLine("Hello, World!") End Sub End Module
print("Hello, World!")
There is no module, no entry point, no imports and no type declarations. A Python file is a program: the interpreter runs its statements from top to bottom. print() is a built-in function, always available, and the parentheses are required — this is the one place Python is stricter than Console.WriteLine, which you can also call with them.
Formatted output
Interpolated strings exist in both, with an f in place of the $ and a different vocabulary inside the braces.
Option Strict On Imports System Module FormatDemo Sub Main() Dim name As String = "Ada" Dim score As Integer = 42 Dim ratio As Double = 0.8756 Console.WriteLine($"Hello, {name}! Score: {score}") Console.WriteLine($"Padded: {score:D5}, rounded: {ratio:F2}") Console.WriteLine(String.Format("{0} scored {1}", name, score)) End Sub End Module
name = "Ada" score = 42 ratio = 0.8756 print(f"Hello, {name}! Score: {score}") print(f"Padded: {score:05d}, rounded: {ratio:.2f}") print("{} scored {}".format(name, score))
$"..." becomes f"...", and the expression inside {} works the same way. The format specifiers do not carry across: .NET's D5 is Python's 05d, F2 is .2f, N0 is ,d, and P1 is .1%. String.Format("{0} ...") has a direct counterpart in "...".format(), though f-strings are what modern Python actually uses.
Controlling what print does
Where Visual Basic gives you two methods, Python gives you one function with two keyword arguments.
Option Strict On Imports System Module PrintDetailDemo Sub Main() Console.Write("Loading") Console.Write("...") Console.WriteLine(" done") Console.WriteLine("a" & vbTab & "b") Console.WriteLine(String.Join(" ", New String() {"x", "y", "z"})) End Sub End Module
print("Loading", end="") print("...", end="") print(" done") print("a", "b", sep="\t") print("x", "y", "z")
Console.Write versus Console.WriteLine becomes print(..., end="") versus plain print(...): the end argument is what gets appended, and it defaults to a newline. print also takes any number of values and joins them with sep, which defaults to a single space — so print("x", "y", "z") does the work of String.Join. Both end and sep are keyword arguments, an idea covered under Functions.
Syntax Fundamentals
Indentation is the syntax
Every End If, Next, End Sub and End Module disappears — and the indentation you were already using becomes load-bearing.
Option Strict On Imports System Module IndentDemo Sub Main() Dim temperature As Integer = 30 If temperature > 25 Then Console.WriteLine("Warm") If temperature > 35 Then Console.WriteLine("Very warm") End If Else Console.WriteLine("Cool") End If End Sub End Module
temperature = 30 if temperature > 25: print("Warm") if temperature > 35: print("Very warm") else: print("Cool")
A block opens with a colon and is delimited by indentation alone; where the indentation goes back out, the block ends. This is not a style rule, it is the grammar, and mixing tabs with spaces is an error rather than a warning. The practical consequence is that you can no longer reformat by hand without changing meaning — but the code you already write, once the End lines are deleted, is almost exactly the shape Python wants.
Comments and docstrings
The comment character changes, and the documentation comment becomes something the program can actually read.
Option Strict On Imports System Module CommentDemo ''' <summary>Doubles a number.</summary> Function Twice(value As Integer) As Integer ' A comment starts with an apostrophe Return value * 2 End Function Sub Main() Console.WriteLine(Twice(21)) End Sub End Module
def twice(value): """Double a number.""" # A comment starts with a hash return value * 2 print(twice(21)) print(twice.__doc__)
A comment is # rather than '. There is no block comment. What replaces '''<summary> is the docstring — a plain string as the first statement of a function, class or module — and it is not a comment at all: it is stored on the object as __doc__, which is how help() and every editor tooltip works. Note also that the function name changed case: Python's convention is snake_case for functions and variables, not PascalCase.
Case sensitivity
The same adjustment C# demands, with one extra wrinkle: Python will let you create a variable by misspelling one.
Option Strict On Imports System Module CaseDemo Sub Main() Dim customerName As String = "Grace" ' All one variable, and the editor rewrites your casing Console.WriteLine(customerName) Console.WriteLine(CustomerName) End Sub End Module
customer_name = "Grace" CustomerName = "Hopper" # Two unrelated variables, and no warning print(customer_name) print(CustomerName)
Python identifiers are case-sensitive, so customerName and CustomerName are separate names. Worse than in a compiled language, assigning to an undeclared name simply creates it — there is no Option Explicit and no declaration step, so a typo on the left of an = is a brand-new variable rather than an error. The conventions that keep you safe are snake_case for variables and functions, PascalCase for classes, UPPER_SNAKE_CASE for constants.
Long lines
The trailing underscore becomes a pair of brackets.
Option Strict On Imports System Module ContinuationDemo Sub Main() Dim total As Integer = 1 + 2 + 3 + 4 + 5 + 6 Dim joined As String = String.Join(", ", _ New String() {"a", "b"}) Console.WriteLine($"{total} / {joined}") End Sub End Module
total = (1 + 2 + 3 + 4 + 5 + 6) joined = ", ".join( ["a", "b"]) print(f"{total} / {joined}")
Inside any bracket — (, [ or { — a Python statement continues across lines freely, which is why the arithmetic above is wrapped in parentheses it does not otherwise need. Outside brackets there is a backslash continuation, \ at end of line, but it is considered poor style and almost never used. The bracket rule covers essentially every real case.
Variables & Types
There is no Dim
The whole left half of a Visual Basic declaration disappears — and so does the guarantee that came with it.
Option Strict On Imports System Module DeclarationDemo Sub Main() Dim count As Integer = 10 Dim label As String = "widget" Dim ready As Boolean = True count = 20 Console.WriteLine($"{count} {label} {ready}") End Sub End Module
count = 10 label = "widget" ready = True count = 20 count = "now a string" print(count, label, ready)
A Python variable is created by assigning to it, and it is a name bound to an object rather than a typed storage slot. The type belongs to the value, not the name, so count can hold an integer on one line and a string on the next and nothing objects. This is the single biggest loss of safety in the move, and the next two rows are about getting some of it back. Note True keeps its capital T here, unlike C#.
Type hints: Option Strict, by convention
Python can be annotated to look exactly like the declaration you are used to — but read the last line before trusting it.
Option Strict On Imports System Imports System.Linq Module TypedDemo Function Repeat(text As String, times As Integer) As String Return String.Concat(Enumerable.Repeat(text, times)) End Function Sub Main() Console.WriteLine(Repeat("ab", 3)) ' Repeat(5, 3) does not compile — the types are checked End Sub End Module
def repeat(text: str, times: int) -> str: return text * times print(repeat("ab", 3)) print(repeat(5, 3)) # 15 — an int, and the hints did not stop it
A type hint puts the type after a colon and the return type after ->, which is the same right-to-left order as text As String. The difference that matters: the interpreter ignores them entirely. repeat(5, 3) is annotated as taking a string, and Python runs it anyway — 5 * 3 is a perfectly good multiplication, so it returns 15 where the annotation promised text. The Visual Basic column will not compile the equivalent line at all. Hints are documentation that external tools — mypy, pyright, your editor — can check; treat them as Option Strict On that only your tooling honors.
Numbers
The fixed-width integer types collapse into one, and that one has no ceiling.
Option Strict On Imports System Module NumberDemo Sub Main() Dim whole As Integer = 2147483647 Dim bigger As Long = 9223372036854775807 Dim precise As Double = 6.7 Dim exact As Decimal = 8.9D Console.WriteLine(whole) Console.WriteLine(bigger) Console.WriteLine(precise + 0.1) Console.WriteLine(exact) End Sub End Module
from decimal import Decimal whole = 2147483647 bigger = 9223372036854775807 huge = bigger * bigger * bigger # no overflow, ever precise = 6.7 exact = Decimal("8.9") print(whole) print(huge) print(precise + 0.1) print(exact)
Short, Integer and Long all become int, which grows to whatever size the value needs — there is no overflow and no OverflowException. Double becomes float and behaves identically, binary rounding and all, which is why 6.7 + 0.1 is untidy in both columns. Decimal is not a built-in: it comes from the standard library and takes a string, because Decimal(8.9) would already have lost the precision you asked for.
Nothing becomes None
Two words that look equivalent and are not: one means "no object", the other means "whatever the default is".
Option Strict On Imports System Module NothingDemo Sub Main() Dim missingText As String = Nothing Dim missingNumber As Integer = Nothing Console.WriteLine(missingText Is Nothing) Console.WriteLine(missingNumber) End Sub End Module
missing_text = None missing_number = None print(missing_text is None) print(missing_number)
None is a single object meaning "nothing here", and testing for it uses is None — which reads exactly like Visual Basic's Is Nothing and means the same thing, identity comparison. The mismatch is on the other side: Dim n As Integer = Nothing stores zero, because Visual Basic's Nothing means "the default value for this type". Python has no such notion — None is None whatever the variable held before.
Converting between types
The C-prefixed functions become built-ins named after the type — and there is no TryParse.
Option Strict On Imports System Module ConversionDemo Sub Main() Dim text As String = "123" Dim parsed As Integer = CInt(text) Dim asText As String = CStr(parsed * 2) Dim asDouble As Double = CDbl(text) Dim value As Integer If Integer.TryParse("oops", value) Then Console.WriteLine(value) Else Console.WriteLine("not a number") End If Console.WriteLine($"{parsed} {asText} {asDouble}") End Sub End Module
text = "123" parsed = int(text) as_text = str(parsed * 2) as_float = float(text) try: value = int("oops") print(value) except ValueError: print("not a number") print(parsed, as_text, as_float)
CInt, CStr, CDbl and CBool become int(), str(), float() and bool(). What has no counterpart is the TryParse pattern: Python has no output parameters, so a conversion that might fail is wrapped in try/except ValueError. That is idiomatic Python rather than a workaround — the community phrase is "easier to ask forgiveness than permission", and exceptions are used for ordinary control flow far more freely than in .NET.
Operators
and, or, not — spelled out
A rare case where Python is closer to Visual Basic than C# is.
Option Strict On Imports System Module LogicalDemo Sub Main() Dim age As Integer = 30 Dim member As Boolean = True If age > 18 AndAlso member Then Console.WriteLine("eligible") If age < 18 OrElse member Then Console.WriteLine("either") If Not member Then Console.WriteLine("not a member") End Sub End Module
age = 30 member = True if age > 18 and member: print("eligible") if age < 18 or member: print("either") if not member: print("not a member")
Python spells its logical operators as words, just as Visual Basic does: and, or, not. They all short-circuit, so they correspond to AndAlso, OrElse and Not. There is no non-short-circuiting pair — & and | exist but are bitwise, so plain And and Or have no equivalent and are not missed. If you have been writing And where you meant AndAlso, the translation quietly fixes it.
Integer division
Python is the only language on this anchor that kept Visual Basic's distinction between the two kinds of division — with one difference in the last column.
Option Strict On Imports System Module DivisionDemo Sub Main() Dim quotient As Integer = 17 \ 5 Dim exact As Double = 17 / 5 Dim remainder As Integer = 17 Mod 5 Dim negative As Integer = -17 \ 5 Console.WriteLine($"{quotient} {exact} {remainder} {negative}") End Sub End Module
quotient = 17 // 5 exact = 17 / 5 remainder = 17 % 5 negative = -17 // 5 print(quotient, exact, remainder, negative)
Visual Basic's \ becomes // and / stays floating-point in both, so the habit transfers intact — a relief after C#, where / silently truncates. Mod becomes %. The one genuine difference is negatives: Visual Basic truncates toward zero, so -17 \ 5 is -3, while Python floors, giving -4. The same split affects %, whose Python result takes the sign of the right operand.
Raising to a power
The operator survives; only the symbol doubles.
Option Strict On Imports System Module PowerDemo Sub Main() Dim squared As Double = 7 ^ 2 Dim root As Double = 81 ^ 0.5 Console.WriteLine($"{squared} {root}") End Sub End Module
squared = 7 ** 2 root = 81 ** 0.5 print(squared, root)
^ becomes **. Unlike C#, which drops the operator entirely in favor of Math.Pow, Python keeps it — and improves on the Visual Basic version by returning an int when both operands are integers, so 7 ** 2 is 49 rather than 49.0. Do not write ^ out of habit: as in C#, it is the bitwise exclusive-or, so 7 ^ 2 quietly evaluates to 5.
Comparisons, and chaining them
Equality gains a character, inequality changes shape, and Python offers something neither Visual Basic nor C# has.
Option Strict On Imports System Module ComparisonDemo Sub Main() Dim value As Integer = 15 If value > 10 AndAlso value < 20 Then Console.WriteLine("in range") End If Console.WriteLine(value = 15) Console.WriteLine(value <> 20) End Sub End Module
value = 15 if 10 < value < 20: print("in range") print(value == 15) print(value != 20)
= for comparison becomes ==, and <> becomes !=. The addition is chained comparison: 10 < value < 20 means exactly what it does in mathematics, evaluates value only once, and short-circuits. It reads better than the AndAlso version and is what Python programmers expect to see for a range test.
Compound assignment
Almost a straight copy — with no ++ in sight.
Option Strict On Imports System Module CompoundDemo Sub Main() Dim total As Integer = 10 total += 5 total -= 2 total *= 3 Dim message As String = "a" message &= "b" Console.WriteLine($"{total} {message}") End Sub End Module
total = 10 total += 5 total -= 2 total *= 3 message = "a" message += "b" print(total, message)
+=, -=, *= and /= are identical; &= for strings becomes +=, and \= becomes //=. Python has no increment operator: there is no ++ or --, so count += 1 is the only way, which is also all Visual Basic ever offered. Anyone who has read C-family code and expected i++ to appear will not find it here.
Strings
Common string operations
The same operations under different names, plus two that stop being methods altogether.
Option Strict On Imports System Module StringMethodDemo Sub Main() Dim text As String = " Visual Basic " Console.WriteLine($"[{text.Trim()}]") Console.WriteLine(text.Trim().ToUpper()) Console.WriteLine(text.Contains("Basic")) Console.WriteLine(text.Trim().Replace(" ", "-")) Console.WriteLine(text.Trim().StartsWith("Visual")) Console.WriteLine(text.Trim().Length) End Sub End Module
text = " Visual Basic " print(f"[{text.strip()}]") print(text.strip().upper()) print("Basic" in text) print(text.strip().replace(" ", "-")) print(text.strip().startswith("Visual")) print(len(text.strip()))
Trimstrip, ToUpperupper, Replacereplace, StartsWithstartswith — all snake_case or simply lower case. Two change shape: Contains becomes the in operator, which reads as English and also works on lists and dictionaries; and .Length becomes the built-in function len(), which likewise works on every sized thing. Python has no equivalent of the Microsoft.VisualBasic family — Mid, Left, Right, InStr — and does not need one, as the next row shows.
Slicing replaces Mid, Left and Right
One notation replaces Substring, Left, Right, indexing and reversal.
Option Strict On Imports System Module SliceDemo Sub Main() Dim text As String = "Visual Basic" Console.WriteLine(text.Substring(0, 6)) Console.WriteLine(text.Substring(7)) Console.WriteLine(text.Substring(text.Length - 5)) Console.WriteLine(text(0)) Console.WriteLine(New String(text.Reverse().ToArray())) End Sub End Module
text = "Visual Basic" print(text[:6]) print(text[7:]) print(text[-5:]) print(text[0]) print(text[::-1])
A slice is [start:stop:step], where each part may be left out. [:6] is Left(text, 6), [7:] is Mid(text, 8), and negative numbers count from the end — so [-5:] is Right(text, 5) and text[-1] is the last character. A step of -1 reverses. The rule to remember is that stop is exclusive, which makes text[:6] six characters, and that slicing never raises for an out-of-range bound — it just gives you what is there.
Multi-line and quoted strings
Python has two quote characters, a raw-string prefix and a triple-quoted form — and you will want all three.
Option Strict On Imports System Module QuotingDemo Sub Main() Dim quoted As String = "She said ""hello""." Dim path As String = "C:\reports\summary.txt" Dim block As String = "line one" & Environment.NewLine & "line two" Console.WriteLine(quoted) Console.WriteLine(path) Console.WriteLine(block) End Sub End Module
quoted = 'She said "hello".' path = r"C:\reports\summary.txt" block = """line one line two""" print(quoted) print(path) print(block)
Single and double quotes are interchangeable, so the simplest way to include a quote is to wrap the string in the other kind. A backslash escapes, as in C#, so a Windows path needs the raw prefix r"..." — Python's answer to C#'s @"...". Triple quotes open a string that runs until the closing triple quote, newlines included, which is what replaces the & Environment.NewLine & chain. The same triple-quoted form is what docstrings use.
Splitting and joining
Splitting is nearly identical; joining is inside out.
Option Strict On Imports System Module SplitDemo Sub Main() Dim line As String = "red,green,blue" Dim parts() As String = line.Split(","c) Console.WriteLine(parts.Length) Console.WriteLine(parts(1)) Console.WriteLine(String.Join(" | ", parts)) End Sub End Module
line = "red,green,blue" parts = line.split(",") print(len(parts)) print(parts[1]) print(" | ".join(parts))
Split becomes split and takes a string rather than a character. String.Join(separator, parts) becomes separator.join(parts) — the separator is the object you call the method on, which looks backwards for about a day and then stops. Calling split() with no argument splits on any run of whitespace and discards empties, which is what you usually want for parsing a line of text.
Strings do not change
Both languages have immutable strings, and both have a preferred way to build one piece by piece — but Python's is not a class.
Option Strict On Imports System Imports System.Text Module ImmutableDemo Sub Main() Dim text As String = "abc" Dim changed As String = text.Replace("a", "z") Console.WriteLine($"{text} {changed}") Dim builder As New StringBuilder() For number As Integer = 1 To 5 builder.Append(number) Next Console.WriteLine(builder.ToString()) End Sub End Module
text = "abc" changed = text.replace("a", "z") print(text, changed) pieces = [] for number in range(1, 6): pieces.append(str(number)) print("".join(pieces))
Every string method returns a new string in both languages, which is why text is unchanged above. Where .NET reaches for StringBuilder, Python collects the pieces in a list and calls "".join() once at the end. It is the same idea — one allocation instead of many — using the collection type it already has. Visual Basic 4.0 and later already made strings immutable, so this is not new; VB6's mutable fixed-length strings are what does not carry over.
Collections
Lists replace both arrays and List(Of T)
One built-in type does the work of arrays, List(Of T) and the VB6 Collection together.
Option Strict On Imports System Imports System.Collections.Generic Module ListDemo Sub Main() Dim fruits As New List(Of String) From {"apple", "banana"} fruits.Add("cherry") fruits.Insert(0, "apricot") fruits.Remove("banana") Console.WriteLine(fruits.Count) Console.WriteLine(fruits(0)) Console.WriteLine(String.Join(", ", fruits)) End Sub End Module
fruits = ["apple", "banana"] fruits.append("cherry") fruits.insert(0, "apricot") fruits.remove("banana") print(len(fruits)) print(fruits[0]) print(", ".join(fruits))
A list is written in square brackets, grows and shrinks freely, and can hold anything — there is no element type to declare. Add becomes append, Count becomes len(), and indexing uses square brackets, which removes the array-versus-method-call ambiguity that parentheses create in Visual Basic. There is no fixed-size array to choose instead, and no ReDim Preserve to worry about: a list simply resizes.
Negative indices and list slices
The same slice notation as strings, now on a list — and it removes most of the LINQ in the left column.
Option Strict On Imports System Imports System.Linq Module IndexDemo Sub Main() Dim numbers() As Integer = {10, 20, 30, 40, 50} Console.WriteLine(numbers(0)) Console.WriteLine(numbers(numbers.Length - 1)) Console.WriteLine(String.Join(", ", numbers.Skip(1).Take(3))) Console.WriteLine(String.Join(", ", numbers.Reverse())) End Sub End Module
numbers = [10, 20, 30, 40, 50] print(numbers[0]) print(numbers[-1]) print(numbers[1:4]) print(numbers[::-1])
numbers[-1] is the last element, [-2] the one before it; there is no UBound and no Length - 1. A slice returns a new list, so [1:4] replaces Skip(1).Take(3) and [::-1] replaces Reverse(). Note that printing a list shows it in brackets with its punctuation, unlike String.Join — that is Python showing you the object rather than its contents as text.
Dictionaries
The literal is far shorter, and the "look it up without exploding" pattern loses its output parameter.
Option Strict On Imports System Imports System.Collections.Generic Module DictionaryDemo Sub Main() Dim ages As New Dictionary(Of String, Integer) From { {"Ada", 36}, {"Grace", 45} } ages("Alan") = 41 For Each entry As KeyValuePair(Of String, Integer) In ages Console.WriteLine($"{entry.Key} is {entry.Value}") Next Console.WriteLine(ages.ContainsKey("Ada")) Dim found As Integer ages.TryGetValue("Nobody", found) Console.WriteLine(found) End Sub End Module
ages = {"Ada": 36, "Grace": 45} ages["Alan"] = 41 for name, age in ages.items(): print(f"{name} is {age}") print("Ada" in ages) print(ages.get("Nobody", 0))
A dictionary literal is {key: value, ...}. Iterating gives you the keys by default, so .items() is what yields pairs — and Python unpacks the pair into two loop variables in the header, where Visual Basic needs a KeyValuePair and two property reads. ContainsKey becomes the in operator. TryGetValue becomes .get(key, default), which returns the default rather than assigning to an output parameter. A VBA reader should note this is the Scripting.Dictionary they know, built in and typed by nothing.
Tuples and unpacking
Returning two things is ordinary in both — Python just stops making a fuss about it.
Option Strict On Imports System Module TupleDemo Function MinimumAndMaximum(values() As Integer) As (Smallest As Integer, Largest As Integer) Dim smallest As Integer = values(0) Dim largest As Integer = values(0) For Each value As Integer In values If value < smallest Then smallest = value If value > largest Then largest = value Next Return (smallest, largest) End Function Sub Main() Dim result = MinimumAndMaximum(New Integer() {4, 9, 1, 7}) Console.WriteLine($"{result.Smallest}..{result.Largest}") End Sub End Module
def minimum_and_maximum(values): return min(values), max(values) low, high = minimum_and_maximum([4, 9, 1, 7]) print(f"{low}..{high}") first, *rest = [1, 2, 3, 4] print(first, rest)
A tuple is a comma-separated list of values, and the parentheses are usually optional: return min(values), max(values) returns one. Unpacking on the left of an = pulls the parts into separate names, and *rest collects whatever is left over. Tuples are immutable, which is what distinguishes them from lists. Python's tuples have no field names as Visual Basic's do — when you want those, the answer is a dataclass or a NamedTuple.
Sets
The same data structure, with the set algebra promoted to operators.
Option Strict On Imports System Imports System.Collections.Generic Module SetDemo Sub Main() Dim seen As New HashSet(Of String) From {"cat", "dog", "cat"} seen.Add("bird") Console.WriteLine(seen.Count) Console.WriteLine(seen.Contains("dog")) Dim other As New HashSet(Of String) From {"dog", "fish"} Dim shared_ As New HashSet(Of String)(seen) shared_.IntersectWith(other) Console.WriteLine(String.Join(", ", shared_)) End Sub End Module
seen = {"cat", "dog", "cat"} seen.add("bird") print(len(seen)) print("dog" in seen) other = {"dog", "fish"} print(seen & other) print(seen | other) print(seen - other)
HashSet(Of T) becomes a set literal in braces — note that {} alone is an empty dictionary, so an empty set is set(). Duplicates collapse on construction in both. Where .NET spells the operations out as mutating methods (IntersectWith, UnionWith, ExceptWith), Python offers &, | and - as non-mutating operators, which read like the mathematics they come from.
Assigning a list does not copy it
This behaves the same way in both languages — it is worth a row because Python has no type declaration to warn you which kind of thing you have.
Option Strict On Imports System Imports System.Collections.Generic Module AliasDemo Sub Main() Dim original As New List(Of Integer) From {1, 2, 3} Dim alias_ As List(Of Integer) = original Dim copy As New List(Of Integer)(original) alias_.Add(4) Console.WriteLine(original.Count) Console.WriteLine(copy.Count) End Sub End Module
original = [1, 2, 3] alias_list = original copy = original[:] alias_list.append(4) print(len(original)) print(len(copy))
A list is a reference in both columns, so alias_list = original gives the same list a second name and appending through one is visible through the other. Copying is original[:] — a full slice — or list(original), matching New List(Of Integer)(original). Both are shallow: the list is new, the objects inside it are not. In Visual Basic the As clause at least tells you a reference type is involved; in Python you have to know, which is why this catches people.
Control Flow
If, ElseIf, Else
The closest translation on the whole page — delete four characters and add a colon.
Option Strict On Imports System Module IfDemo Sub Main() Dim score As Integer = 72 If score >= 90 Then Console.WriteLine("A") ElseIf score >= 70 Then Console.WriteLine("B") ElseIf score >= 50 Then Console.WriteLine("C") Else Console.WriteLine("F") End If End Sub End Module
score = 72 if score >= 90: print("A") elif score >= 70: print("B") elif score >= 50: print("C") else: print("F")
ElseIf becomes elif, the Then and the End If go away, and each branch header ends with a colon. The condition needs no parentheses, unlike C#. This is the shape most Visual Basic code already has, so most If ladders port almost mechanically.
Truthiness
Everything in Python can be used as a condition, and this is the most common source of surprise for anyone arriving from Option Strict On.
Option Strict On Imports System Imports System.Collections.Generic Module TruthDemo Sub Main() Dim items As New List(Of String) Dim text As String = "" Dim count As Integer = 0 ' Option Strict On requires each test to be spelled out If items.Count = 0 Then Console.WriteLine("no items") If String.IsNullOrEmpty(text) Then Console.WriteLine("no text") If count = 0 Then Console.WriteLine("zero") End Sub End Module
items = [] text = "" count = 0 if not items: print("no items") if not text: print("no text") if not count: print("zero")
An empty list, an empty string, an empty dictionary, 0, 0.0 and None are all falsy; everything else is truthy. So if not items means "if the list is empty". It is genuinely idiomatic and worth adopting — but note what it costs you: if not count cannot tell zero from None, and if not text cannot tell an empty string from a missing one. When that distinction matters, test explicitly with is None.
Select Case becomes match
Python gained a match statement in 3.10, and it lines up with Select Case more closely than you might expect.
Option Strict On Imports System Module SelectDemo Function Describe(code As Integer) As String Select Case code Case 1 Return "one" Case 2, 3 Return "two or three" Case 4 To 6 Return "four to six" Case Else Return "something else" End Select End Function Sub Main() Console.WriteLine(Describe(1)) Console.WriteLine(Describe(3)) Console.WriteLine(Describe(5)) Console.WriteLine(Describe(9)) End Sub End Module
def describe(code): match code: case 1: return "one" case 2 | 3: return "two or three" case value if 4 <= value <= 6: return "four to six" case _: return "something else" print(describe(1)) print(describe(3)) print(describe(5)) print(describe(9))
Case 2, 3 becomes case 2 | 3, and Case Else becomes case _. A range test has no direct form, so it becomes a capture with a guard: case value if 4 <= value <= 6. Like Select Case and unlike C#, no branch falls through to the next. match can also destructure lists, dictionaries and objects, which puts it closer to C#'s pattern matching than to Select Case — but for the plain value-dispatch shown here, a dictionary of functions is still the more common Python idiom.
If() as an expression
Both languages have a conditional expression; Python writes it in the order you would say it aloud.
Option Strict On Imports System Module ConditionalDemo Sub Main() Dim score As Integer = 72 Dim grade As String = If(score >= 60, "pass", "fail") Dim supplied As String = Nothing Dim label As String = If(supplied, "(unnamed)") Console.WriteLine($"{grade} / {label}") End Sub End Module
score = 72 grade = "pass" if score >= 60 else "fail" supplied = None label = supplied if supplied is not None else "(unnamed)" print(f"{grade} / {label}")
If(condition, whenTrue, whenFalse) becomes whenTrue if condition else whenFalse — the value first, the test in the middle. It short-circuits, evaluating only the branch it takes. The two-argument If(value, fallback) has no dedicated operator; supplied or "(unnamed)" is the short idiom, but it falls back on any falsy value, so an empty string would also be replaced. Where that matters, spell out is not None as above.
Loops
For ... Next becomes for ... in range
A counted loop becomes a loop over a sequence of numbers — and the ending value shifts by one.
Option Strict On Imports System Module ForDemo Sub Main() For index As Integer = 1 To 5 Console.Write(index & " ") Next Console.WriteLine() For countdown As Integer = 10 To 0 Step -2 Console.Write(countdown & " ") Next Console.WriteLine() End Sub End Module
for index in range(1, 6): print(index, end=" ") print() for countdown in range(10, -1, -2): print(countdown, end=" ") print()
range(start, stop, step) produces numbers from start up to but not including stop, so Visual Basic's inclusive To 5 becomes range(1, 6). This is the off-by-one to watch, and it is the same rule as slicing. range(n) with one argument counts from 0, which is what you want for indexing. A negative step counts down, and the exclusive stop applies there too — To 0 becomes -1.
For Each, with and without an index
The plain iteration loses its type annotation; the indexed one gains a built-in that has no .NET equivalent.
Option Strict On Imports System Imports System.Collections.Generic Module ForEachDemo Sub Main() Dim words As New List(Of String) From {"alpha", "beta", "gamma"} For Each word As String In words Console.WriteLine(word.ToUpper()) Next For index As Integer = 0 To words.Count - 1 Console.WriteLine($"{index}: {words(index)}") Next End Sub End Module
words = ["alpha", "beta", "gamma"] for word in words: print(word.upper()) for index, word in enumerate(words): print(f"{index}: {word}")
For Each word As String In words becomes for word in words. When you need the position too, enumerate() yields (index, value) pairs which unpack straight into two loop variables — no counting loop, no Count - 1, no indexing. It takes a start= argument if you want to number from 1. Reaching for range(len(words)) instead is the classic sign of code written by someone who has not met enumerate yet.
While, and the missing Do ... Loop
The top-tested loop translates directly. The bottom-tested one has to be built by hand.
Option Strict On Imports System Module WhileDemo Sub Main() Dim remaining As Integer = 3 While remaining > 0 Console.WriteLine($"remaining {remaining}") remaining -= 1 End While Dim attempt As Integer = 0 Do attempt += 1 Console.WriteLine($"attempt {attempt}") Loop Until attempt >= 2 End Sub End Module
remaining = 3 while remaining > 0: print(f"remaining {remaining}") remaining -= 1 attempt = 0 while True: attempt += 1 print(f"attempt {attempt}") if attempt >= 2: break
While ... End While becomes while ...:. Python has no do ... while at all, so the entire Do ... Loop While / Loop Until family becomes while True: with a break at the point the condition would have been tested. It looks worse than it is: the test ends up written exactly where it happens, and the pattern is common enough in Python that nobody blinks at it.
break, continue, and a loop that has an else
Two familiar keywords, and one construct with no counterpart anywhere in .NET.
Option Strict On Imports System Module BreakDemo Sub Main() Dim numbers() As Integer = {4, 9, 16, 25} Dim foundOdd As Boolean = False For Each number As Integer In numbers If number Mod 2 = 0 Then Continue For foundOdd = True Console.WriteLine($"first odd: {number}") Exit For Next If Not foundOdd Then Console.WriteLine("no odd numbers") End Sub End Module
numbers = [4, 9, 16, 25] for number in numbers: if number % 2 == 0: continue print(f"first odd: {number}") break else: print("no odd numbers")
Exit For becomes break and Continue For becomes continue, both acting on the innermost loop. The addition is the loop else: it runs only if the loop finished without hitting break, which removes the found-it flag the anchor column has to carry. Read it as "else, no break" rather than "else" — the keyword is admittedly badly chosen — and it is exactly the right tool for a search loop.
Functions
Sub and Function both become def
One keyword covers both, and a function that returns nothing still returns something.
Option Strict On Imports System Module FunctionDemo Sub Announce(message As String) Console.WriteLine($"** {message} **") End Sub Function Add(left As Integer, right As Integer) As Integer Return left + right End Function Sub Main() Announce("starting") Console.WriteLine(Add(2, 3)) End Sub End Module
def announce(message): print(f"** {message} **") def add(left, right): return left + right announce("starting") print(add(2, 3))
Sub and Function both become def; the difference is only whether there is a return. A def with no return gives back None, so there is no void to declare and nothing stops you assigning the result — a small trap when a function you thought returned a value does not. Note the order: Python executes top to bottom, so a function must be defined before the line that calls it, unlike a Module where order never mattered.
Optional and named arguments
Both features exist in both languages, and Python's spelling is the shorter one.
Option Strict On Imports System Module ArgumentDemo Function Greet(name As String, Optional greeting As String = "Hello", Optional punctuation As String = "!") As String Return $"{greeting}, {name}{punctuation}" End Function Sub Main() Console.WriteLine(Greet("Ada")) Console.WriteLine(Greet("Grace", "Welcome")) Console.WriteLine(Greet("Alan", punctuation:=".")) End Sub End Module
def greet(name, greeting="Hello", punctuation="!"): return f"{greeting}, {name}{punctuation}" print(greet("Ada")) print(greet("Grace", "Welcome")) print(greet("Alan", punctuation="."))
The Optional keyword disappears — a default value is what makes a parameter optional — and a named argument uses name= rather than name:=. As in Visual Basic, parameters with defaults must come last. Named arguments are used far more heavily in Python than in .NET, and a function with more than two or three parameters is normally called with them for readability.
ParamArray, and its keyword twin
ParamArray has a direct equivalent — and a second form Visual Basic has nothing like.
Option Strict On Imports System Module VariadicDemo Function SumAll(ParamArray values() As Integer) As Integer Dim total As Integer = 0 For Each value As Integer In values total += value Next Return total End Function Sub Main() Console.WriteLine(SumAll(1, 2, 3)) Console.WriteLine(SumAll()) End Sub End Module
def sum_all(*values): return sum(values) def describe(**details): return ", ".join(f"{key}={value}" for key, value in details.items()) print(sum_all(1, 2, 3)) print(sum_all()) print(describe(colour="red", size=3))
ParamArray values() As Integer becomes *values, which collects the extra positional arguments into a tuple. **details is the half with no counterpart: it collects any extra named arguments into a dictionary, which is how so many Python libraries accept open-ended options. The same two stars work at the call site, spreading a list or a dictionary back out into arguments — sum_all(*[1, 2, 3]).
There is no ByRef
Python passes every argument the same way, and what happens next depends on the object rather than the call.
Option Strict On Imports System Imports System.Collections.Generic Module ByRefDemo Sub Double_(ByRef value As Integer) value *= 2 End Sub Sub AddItem(items As List(Of Integer)) items.Add(99) End Sub Sub Main() Dim number As Integer = 21 Double_(number) Console.WriteLine(number) Dim numbers As New List(Of Integer) From {1} AddItem(numbers) Console.WriteLine(numbers.Count) End Sub End Module
def double_(value): value *= 2 # rebinds the local name only return value def add_item(items): items.append(99) # mutates the caller's list number = 21 double_(number) print(number) number = double_(number) print(number) numbers = [1] add_item(numbers) print(len(numbers))
There is no ByRef, no ref, no out. Assigning to a parameter rebinds the local name and the caller sees nothing — which is why double_ has to return. But calling a mutating method on a passed object changes the object the caller holds, so add_item works. The rule is not "value versus reference" but "rebinding versus mutating", and the practical advice is to return new values rather than modify arguments in place.
Lambdas
Python has lambdas, but they are deliberately limited — and most of the time you want the other thing on this line.
Option Strict On Imports System Imports System.Linq Module LambdaDemo Sub Main() Dim numbers() As Integer = {5, 3, 9, 1} Dim doubled = numbers.Select(Function(number) number * 2) Dim sorted = numbers.OrderByDescending(Function(number) number) Console.WriteLine(String.Join(", ", doubled)) Console.WriteLine(String.Join(", ", sorted)) End Sub End Module
numbers = [5, 3, 9, 1] doubled = [number * 2 for number in numbers] sorted_numbers = sorted(numbers, key=lambda number: number, reverse=True) print(doubled) print(sorted_numbers)
Function(number) number * 2 becomes lambda number: number * 2. A Python lambda holds one expression and nothing more — no statements, no multi-line body, so there is no equivalent of the multi-line Sub() ... End Sub form. Where Visual Basic would reach for a lambda inside Select, Python usually reaches for a comprehension instead, as the first line shows. Lambdas survive mainly as the key= argument to sorted, min and max.
Classes & Objects
A class and its constructor
The constructor gets a fixed name, and the instance you are working on becomes an explicit first parameter.
Option Strict On Imports System Public Class Person Private ReadOnly _name As String Private ReadOnly _age As Integer Public Sub New(name As String, age As Integer) _name = name _age = age End Sub Public Function Describe() As String Return $"{_name}, age {_age}" End Function End Class Module ClassDemo Sub Main() Dim person As New Person("Ada", 36) Console.WriteLine(person.Describe()) End Sub End Module
class Person: def __init__(self, name, age): self.name = name self.age = age def describe(self): return f"{self.name}, age {self.age}" person = Person("Ada", 36) print(person.describe())
Public Sub New becomes __init__, one of the "dunder" (double-underscore) methods Python uses for everything the language calls on your behalf. Me becomes self — and unlike Me, it is written out as the first parameter of every method and used explicitly for every field access. There is no New keyword at the call site: Person("Ada", 36) calls the class. Fields are created by assigning to self.something inside __init__; there is no separate declaration.
Properties
Properties exist in Python too — as a decorator on an ordinary method.
Option Strict On Imports System Public Class Temperature Private _celsius As Double Public Property Celsius As Double Get Return _celsius End Get Set(value As Double) _celsius = Math.Max(value, -273.15) End Set End Property Public ReadOnly Property Fahrenheit As Double Get Return _celsius * 9.0 / 5.0 + 32 End Get End Property End Class Module PropertyDemo Sub Main() Dim reading As New Temperature() reading.Celsius = 100.0 Console.WriteLine(reading.Fahrenheit) reading.Celsius = -500 Console.WriteLine(reading.Celsius) End Sub End Module
class Temperature: def __init__(self): self._celsius = 0.0 @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): self._celsius = max(value, -273.15) @property def fahrenheit(self): return self._celsius * 9.0 / 5.0 + 32 reading = Temperature() reading.celsius = 100.0 print(reading.fahrenheit) reading.celsius = -500 print(reading.celsius)
@property turns a method into something read as an attribute, and @name.setter supplies the write half; a property with only the getter is read-only, matching ReadOnly Property. The important cultural difference is that Python does not wrap fields in properties by default: a plain self.celsius is normal, and you convert it to a property later if you need validation — the calling code does not change. The leading underscore on _celsius is a convention meaning "internal", not an access modifier; Python has no Private.
Inheritance and overriding
Everything is overridable, so three of the four keywords in the left column simply have nothing to translate to.
Option Strict On Imports System Public Class Animal Protected ReadOnly Name As String Public Sub New(name As String) Me.Name = name End Sub Public Overridable Function Speak() As String Return $"{Name} makes a sound" End Function End Class Public Class Dog Inherits Animal Public Sub New(name As String) MyBase.New(name) End Sub Public Overrides Function Speak() As String Return $"{Name} barks" End Function End Class Module InheritanceDemo Sub Main() Dim pets As Animal() = {New Animal("Thing"), New Dog("Rex")} For Each pet As Animal In pets Console.WriteLine(pet.Speak()) Next End Sub End Module
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound" class Dog(Animal): def speak(self): return f"{self.name} barks" for pet in [Animal("Thing"), Dog("Rex")]: print(pet.speak())
Inherits Animal becomes (Animal) after the class name. Overridable and Overrides have no equivalent — every method can be overridden and redefining one is all it takes, which means nothing warns you if you misspell the name and accidentally add a method rather than replacing one. MyBase becomes super(), and Dog here does not need a constructor at all: leaving __init__ out inherits the parent's.
Interfaces become duck typing
The interface, the two Implements clauses and the typed list all disappear, and the loop still works.
Option Strict On Imports System Imports System.Collections.Generic Public Interface IGreeter Function Greet(name As String) As String End Interface Public Class Formal Implements IGreeter Public Function Greet(name As String) As String Implements IGreeter.Greet Return $"Good day, {name}." End Function End Class Public Class Casual Implements IGreeter Public Function Greet(name As String) As String Implements IGreeter.Greet Return $"Hi {name}!" End Function End Class Module DuckDemo Sub Main() Dim greeters As New List(Of IGreeter) From {New Formal(), New Casual()} For Each greeter As IGreeter In greeters Console.WriteLine(greeter.Greet("Ada")) Next End Sub End Module
class Formal: def greet(self, name): return f"Good day, {name}." class Casual: def greet(self, name): return f"Hi {name}!" for greeter in [Formal(), Casual()]: print(greeter.greet("Ada"))
Python does not check that an object implements an interface; it calls the method and sees what happens. Anything with a greet method is usable here, declared relationship or not — this is duck typing, and it is why so much Python code has no type hierarchy at all. What you give up is the compiler telling you a class forgot a member. When you want that back, the abc module's ABC and @abstractmethod raise on instantiation if a method is missing, and typing.Protocol lets a type checker verify the shape without inheritance.
Operator overloading and ToString
The same two customizations, both spelled as methods with reserved names.
Option Strict On Imports System Public Structure Money Public ReadOnly Amount As Decimal Public Sub New(amount As Decimal) Me.Amount = amount End Sub Public Shared Operator +(left As Money, right As Money) As Money Return New Money(left.Amount + right.Amount) End Operator Public Overrides Function ToString() As String Return $"{Amount:F2}" End Function End Structure Module OperatorDemo Sub Main() Dim total As Money = New Money(1.5D) + New Money(2.25D) Console.WriteLine(total) End Sub End Module
from decimal import Decimal class Money: def __init__(self, amount): self.amount = Decimal(amount) def __add__(self, other): return Money(self.amount + other.amount) def __str__(self): return f"{self.amount:.2f}" total = Money("1.5") + Money("2.25") print(total)
Operator + becomes __add__, and there is one of these for every operator — __sub__, __mul__, __eq__, __lt__. Overrides Function ToString becomes __str__, which print() and str() call; its companion __repr__ is what the interactive prompt and a printed list show, and is meant for developers rather than users. This family of dunder methods is how Python exposes essentially all of its built-in behavior, including __len__ for len() and __iter__ for for ... in.
Modules & the Standard Library
Imports becomes import
Three shapes of import, matching the three things Imports can do for you.
Option Strict On Imports System Imports System.Math Imports Builder = System.Text.StringBuilder Module ImportDemo Sub Main() Console.WriteLine(Sqrt(16)) Console.WriteLine(Math.PI > 3) Dim text As New Builder() text.Append("built") Console.WriteLine(text.ToString()) End Sub End Module
import math from math import sqrt import datetime as dt print(sqrt(16)) print(math.pi > 3) print(dt.date(2026, 1, 1).year)
import math brings in the module and you reach through it, which is the default and the clearest. from math import sqrt brings one name into your file, matching Imports System.Math. import datetime as dt is the aliased form, matching Imports Builder = .... Unlike a Visual Basic project, there are no project-level imports: every file states what it needs, so nothing is quietly available because a project setting says so.
What replaces the .NET base class library
The habit worth forming: before writing a loop, check whether the standard library already has it.
Option Strict On Imports System Imports System.Collections.Generic Imports System.Linq Module LibraryDemo Sub Main() Dim words() As String = {"pear", "apple", "pear", "fig"} Dim counts As New Dictionary(Of String, Integer) For Each word As String In words If counts.ContainsKey(word) Then counts(word) += 1 Else counts(word) = 1 End If Next For Each entry In counts.OrderBy(Function(pair) pair.Key) Console.WriteLine($"{entry.Key}: {entry.Value}") Next Console.WriteLine(New Random(42).Next(1, 7) >= 1) End Sub End Module
from collections import Counter import random words = ["pear", "apple", "pear", "fig"] counts = Counter(words) for word, count in sorted(counts.items()): print(f"{word}: {count}") random.seed(42) print(random.randint(1, 6) >= 1)
Python's standard library is the counterpart of the base class library, and it is unusually broad — collections, itertools, datetime, json, csv, re, pathlib, sqlite3, statistics all ship with the interpreter. Counter above replaces the whole tally loop. Anything beyond it comes from PyPI via pip install, which is the equivalent of NuGet — and for the reader coming from Excel, openpyxl and pandas are the two names to look up first.
The if __name__ line
The line you will see at the bottom of nearly every Python file, and what it is protecting against.
Option Strict On Imports System Module EntryPointDemo Function Add(left As Integer, right As Integer) As Integer Return left + right End Function Sub Main() Console.WriteLine(Add(2, 3)) End Sub End Module
def add(left, right): return left + right def main(): print(add(2, 3)) if __name__ == "__main__": main()
Because importing a module runs it, any statement at the top level of a file executes when someone imports it. __name__ is "__main__" only when the file is run directly, so the guard means "do this when I am the program, not when I am a library". It is the closest thing Python has to Sub Main, and unlike Sub Main it is a convention rather than a rule — the short examples on this page skip it, as short scripts generally do.
Error Handling
Try/Catch becomes try/except
The same construct with a different keyword — plus a clause .NET does not have.
Option Strict On Imports System Module TryDemo Sub Main() Try Dim numbers() As Integer = {1, 2, 3} Console.WriteLine(numbers(10)) Catch error_ As IndexOutOfRangeException Console.WriteLine($"Out of range: {error_.Message}") Catch error_ As Exception Console.WriteLine($"Something else: {error_.Message}") Finally Console.WriteLine("always runs") End Try End Sub End Module
try: numbers = [1, 2, 3] print(numbers[10]) except IndexError as error: print(f"Out of range: {error}") except Exception as error: print(f"Something else: {error}") else: print("only if nothing was raised") finally: print("always runs")
Catch name As TypeName becomes except TypeName as name, and Finally becomes finally. Order still matters, most specific first. The addition is else, which runs only when the try block completed without raising — it keeps the "if this worked, now do that" code out of the try, so an exception from it is not caught by mistake. As with C#, On Error Resume Next and On Error GoTo have no counterpart and must be rewritten rather than translated.
Raising your own exception
Defining an exception is just the inheritance you already saw, applied to one particular base class.
Option Strict On Imports System Public Class InsufficientFundsException Inherits Exception Public ReadOnly Property Shortfall As Decimal Public Sub New(shortfall As Decimal) MyBase.New($"Short by {shortfall}") Me.Shortfall = shortfall End Sub End Class Module RaiseDemo Sub Withdraw(balance As Decimal, amount As Decimal) If amount > balance Then Throw New InsufficientFundsException(amount - balance) End If Console.WriteLine($"Withdrew {amount}") End Sub Sub Main() Try Withdraw(50D, 75D) Catch error_ As InsufficientFundsException Console.WriteLine($"{error_.Message} (short {error_.Shortfall})") End Try End Sub End Module
class InsufficientFundsError(Exception): def __init__(self, shortfall): super().__init__(f"Short by {shortfall}") self.shortfall = shortfall def withdraw(balance, amount): if amount > balance: raise InsufficientFundsError(amount - balance) print(f"Withdrew {amount}") try: withdraw(50, 75) except InsufficientFundsError as error: print(f"{error} (short {error.shortfall})")
Throw New becomes raise, and the custom type inherits with (Exception). MyBase.New(message) becomes super().__init__(message). Two naming notes: Python convention ends exception class names in Error, not Exception; and printing the exception object gives you the message, so there is no .Message property to reach for. A bare raise inside an except re-raises the current exception with its traceback intact, matching a bare Throw.
Using becomes with
The construct that guarantees cleanup keeps its shape and loses its name.
Option Strict On Imports System Imports System.IO Module UsingDemo Sub Main() Dim contents As String = "" Using writer As New StringWriter() writer.WriteLine("first line") writer.WriteLine("second line") contents = writer.ToString() End Using Console.WriteLine(contents.Trim()) End Sub End Module
import io with io.StringIO() as buffer: buffer.write("first line\n") buffer.write("second line\n") contents = buffer.getvalue() print(contents.strip()) print(buffer.closed) # True — leaving the block released it
Using ... End Using becomes with ...:, and it guarantees the same thing — the resource is released when the block ends, exception or not. Where .NET requires IDisposable, Python requires the object to define __enter__ and __exit__; that pair is called a context manager. The most common use by far is with open("file.txt") as handle:, which is the standard way to read a file and the reason a Python programmer almost never has to remember to close one.
Ask forgiveness, not permission
The same two situations, handled the way each community actually handles them.
Option Strict On Imports System Imports System.Collections.Generic Module PermissionDemo Sub Main() Dim ages As New Dictionary(Of String, Integer) From {{"Ada", 36}} ' Check first — the .NET habit If ages.ContainsKey("Grace") Then Console.WriteLine(ages("Grace")) Else Console.WriteLine("not found") End If Dim value As Integer If Integer.TryParse("12x", value) Then Console.WriteLine(value) Else Console.WriteLine("not a number") End If End Sub End Module
ages = {"Ada": 36} try: print(ages["Grace"]) except KeyError: print("not found") try: print(int("12x")) except ValueError: print("not a number")
.NET culture checks first — ContainsKey, TryParse, Exists — partly because exceptions there are genuinely expensive. Python culture attempts the operation and catches the failure, a style its community abbreviates as EAFP: easier to ask forgiveness than permission. Exceptions in Python are cheap, and the check-first version has a race in it whenever the thing being checked can change. Both styles work; the point is that Python code you read will be written the second way, so it needs to look normal to you.
Comprehensions & Generators
Comprehensions replace LINQ
The single most characteristic line of Python, and the one that replaces most of what you use LINQ for.
Option Strict On Imports System Imports System.Linq Module ComprehensionDemo Sub Main() Dim numbers() As Integer = {5, 3, 9, 1, 7, 2} Dim result = numbers. Where(Function(number) number > 2). Select(Function(number) number * 10). ToList() Console.WriteLine(String.Join(", ", result)) End Sub End Module
numbers = [5, 3, 9, 1, 7, 2] result = [number * 10 for number in numbers if number > 2] print(result)
A list comprehension reads "the expression, for each item, where the condition" — Select at the front, Where at the back, one line, no lambdas. The same brackets change the result type: {...} gives a set, {key: value for ...} gives a dictionary, and (...) gives a lazy generator. Where LINQ is lazy by default and needs ToList(), a list comprehension is eager and builds the list immediately.
Aggregating a sequence
The LINQ aggregates become built-in functions that take the sequence rather than methods hanging off it.
Option Strict On Imports System Imports System.Linq Module AggregateDemo Sub Main() Dim numbers() As Integer = {5, 3, 9, 1, 7, 2} Console.WriteLine(numbers.Sum()) Console.WriteLine(numbers.Max()) Console.WriteLine(numbers.Average()) Console.WriteLine(numbers.Any(Function(number) number > 8)) Console.WriteLine(numbers.All(Function(number) number > 0)) Console.WriteLine(String.Join(", ", numbers.OrderBy(Function(number) number))) End Sub End Module
numbers = [5, 3, 9, 1, 7, 2] print(sum(numbers)) print(max(numbers)) print(sum(numbers) / len(numbers)) print(any(number > 8 for number in numbers)) print(all(number > 0 for number in numbers)) print(sorted(numbers))
Sum, Max, Min, Any and All become sum(), max(), min(), any() and all(), called around the sequence instead of on it. Average has no built-in — it is sum(x) / len(x), or statistics.mean. OrderBy becomes sorted(), which returns a new list, while list.sort() sorts in place. The argument to any and all above is a generator expression — a comprehension in parentheses, evaluated lazily, which is why any can stop at the first match.
Iterator functions
Both languages let a function produce values lazily, and the keyword is nearly the same.
Option Strict On Imports System Imports System.Collections.Generic Imports System.Linq Module GeneratorDemo Iterator Function Fibonacci() As IEnumerable(Of Integer) Dim previous As Integer = 0 Dim current As Integer = 1 Do Yield previous Dim next_ As Integer = previous + current previous = current current = next_ Loop End Function Sub Main() Console.WriteLine(String.Join(" ", Fibonacci().Take(8))) End Sub End Module
import itertools def fibonacci(): previous, current = 0, 1 while True: yield previous previous, current = current, previous + current print(list(itertools.islice(fibonacci(), 8)))
Iterator Function ... Yield becomes a plain def containing yield — the presence of yield is what makes it a generator, so there is no Iterator keyword to write. Both suspend at the yield and resume where they left off, so an infinite sequence is fine as long as the caller stops taking. Take(8) becomes itertools.islice(..., 8). Note the Python column also swaps two variables in one line, previous, current = current, previous + current, which is tuple packing and unpacking doing the work of a temporary.
Functions are values
The AddressOf operator and the Func(Of ...) type both disappear, because a function is already a value.
Option Strict On Imports System Imports System.Collections.Generic Module FunctionValueDemo Function Twice(value As Integer) As Integer Return value * 2 End Function Function Negate(value As Integer) As Integer Return -value End Function Sub Main() Dim operations As New Dictionary(Of String, Func(Of Integer, Integer)) From { {"twice", AddressOf Twice}, {"negate", AddressOf Negate} } For Each name As String In New String() {"twice", "negate"} Console.WriteLine($"{name}: {operations(name)(21)}") Next End Sub End Module
def twice(value): return value * 2 def negate(value): return -value operations = {"twice": twice, "negate": negate} for name in ["twice", "negate"]: print(f"{name}: {operations[name](21)}")
Writing a function's name without parentheses gives you the function itself; adding parentheses calls it. There is no AddressOf and no delegate type to declare, so a dictionary of functions is just a dictionary. This is also the idiomatic replacement for a long Select Case that dispatches to different behavior — build a table once and index it, rather than testing values in order.
⚠ Gotchas for Visual Basic Programmers
⚠ A default argument is created once
The most famous Python surprise, and it has no Visual Basic counterpart because .NET simply forbids the thing that causes it.
Option Strict On Imports System Imports System.Collections.Generic Module DefaultDemo Function AddItem(item As String, Optional target As List(Of String) = Nothing) As List(Of String) If target Is Nothing Then target = New List(Of String)() target.Add(item) Return target End Function Sub Main() Console.WriteLine(String.Join(", ", AddItem("a"))) Console.WriteLine(String.Join(", ", AddItem("b"))) End Sub End Module
def add_item_wrong(item, target=[]): target.append(item) return target def add_item(item, target=None): if target is None: target = [] target.append(item) return target print(add_item_wrong("a")) print(add_item_wrong("b")) # ["a", "b"] — the SAME list print(add_item("a")) print(add_item("b"))
A default value is evaluated once, when the function is defined — not on each call. So target=[] creates one list that every call without an argument shares, and it accumulates. Visual Basic cannot hit this because an Optional parameter's default must be a compile-time constant, so it can never be a list. The fix is the idiom shown: default to None and build the real value inside. Treat any mutable default — list, dictionary, set — as a bug.
⚠ is compares identity, not value
This one keyword means the same thing it means in Visual Basic — which is exactly why it is a trap.
Option Strict On Imports System Module IsDemo Sub Main() Dim left As String = "hello" Dim right As String = "hel" & "lo" Console.WriteLine(left = right) Console.WriteLine(left Is right) End Sub End Module
left = [1, 2, 3] right = [1, 2, 3] print(left == right) # True — same contents print(left is right) # False — different objects print(left is left) # True
Python's is is identity comparison, the same as Visual Basic's Is, and == is value comparison, the same as Visual Basic's = on strings. The trap is that small integers and short strings are often interned — reused rather than recreated — so a is b can be True for two separately computed values and then quietly become False when the numbers get larger. Never use is to compare values. Use it only against None, True and False, which really are single objects.
⚠ Nothing is really constant
There is no Const, and the convention that replaces it is enforced by nothing but manners.
Option Strict On Imports System Module ConstantDemo Const MaximumRetries As Integer = 3 Sub Main() Console.WriteLine(MaximumRetries) ' MaximumRetries = 5 ' would not compile End Sub End Module
MAXIMUM_RETRIES = 3 print(MAXIMUM_RETRIES) MAXIMUM_RETRIES = 5 # nothing stops this print(MAXIMUM_RETRIES)
Python has no constant declaration. The convention is UPPER_SNAKE_CASE, which every Python programmer reads as "do not reassign this", and which some type checkers will enforce if you annotate it as Final. But at runtime it is an ordinary variable. The same is true of privacy: a leading underscore means "internal, do not touch", and nothing prevents touching it. Python's stance throughout is that the language states intent and trusts the programmer, where .NET has the compiler enforce it.
⚠ Zero-based, and no Option Base
The array-bound arithmetic you learned for .NET applies here too — with one extra habit worth unlearning.
Option Strict On Imports System Module BaseDemo Sub Main() Dim slots(5) As Integer slots(5) = 99 Console.WriteLine(slots.Length) Console.WriteLine(UBound(slots)) Console.WriteLine(slots(5)) End Sub End Module
slots = [0] * 5 slots[4] = 99 print(len(slots)) print(len(slots) - 1) print(slots[4]) print(slots[-1])
Dim slots(5) is six slots because the number is the upper bound; [0] * 5 is five because the number is the count. UBound becomes len(x) - 1, and LBound is always 0. What is genuinely new is that you rarely need either: slots[-1] is the last element directly, and for item in slots avoids indexing altogether. If you are carrying VB6 habits, note that Option Base 1 has no equivalent in .NET or Python, and the 1-based Mid/InStr family has none here either.
⚠ A typo waits until that line runs
The most consequential difference on this page, and the one with no code-level fix.
Option Strict On Imports System Module LateErrorDemo Sub Main() Dim total As Integer = 10 If total > 100 Then ' A misspelling here fails to COMPILE, ' so this program never ships Console.WriteLine(total) End If Console.WriteLine("finished") End Sub End Module
total = 10 if total > 100: print(totl) # misspelled — but this line never runs print("finished")
Python compiles a file only far enough to check its syntax. A misspelled name, a call with the wrong number of arguments, an attribute that does not exist — all of these are found when the line executes, and a branch that never runs is never checked. There is no Option Strict, no Option Explicit and no compiler to lean on. What replaces them is discipline the language does not enforce for you: tests that exercise every branch, type hints checked by mypy or pyright, and a linter such as ruff in the editor. Set those up on day one rather than after the first production surprise.
⚠ No My namespace, no MsgBox, no Excel
Everything the My namespace and the Microsoft.VisualBasic functions gave you has an equivalent — it is just spread across the standard library.
Option Strict On Imports System Module MyDemo Sub Main() ' My.Computer.Name, My.Application.Info, MsgBox, InputBox ' and the Excel object model are all Visual Basic conveniences Console.WriteLine(Environment.MachineName.Length > 0) Console.WriteLine(IsNumeric("42")) Console.WriteLine(Now.Year > 2000) End Sub End Module
import platform import datetime print(len(platform.node()) > 0) print("42".isdigit()) print(datetime.datetime.now().year > 2000)
My.Computer.Name is platform.node(), My.Computer.FileSystem is pathlib and shutil, IsNumeric is str.isdigit() or a try: float(x), Now is datetime.datetime.now(), and MsgBox/InputBox have no equivalent in a script at all — the console, or a real UI library, replaces them. For the VBA reader the biggest one is the Excel object model: openpyxl reads and writes workbooks directly, and pandas reads a sheet into a table you can work on without looping over cells.