본문 바로가기
개발 언어/파이선

Pythonic Code 란? 파이서닉 코드 알아보기

by Marco Backman 2025. 9. 21.

 

개발자라면 클린 전반적으로 지켜지는 클린코드 규약을 따라야 하는데요. 파이선 개발에 있어서 기본적인 규칙을 따라야 한다는 클린코드의 관점을 파이썬스러움(Pythonic Code)이라고 표현을 합니다.

 

이런 파이선 특유의 명칭이 생긴 이유는 다른 언어와 차별화 된 파이선만의 기능을 최대한 활용해서 깨끗한 코드를 만들 수 있기 때문인데요.

 

이제부터 각각의 파이썬스러운 코드 규칙에 대해 설명하도록 하겠습니다.


 

1.  join, split을 활용해 파이서닉 스러운 string, list 만들기

 

파이써닉하지 못한 join: 문장 끝에 필요없는 스페이스가 추가되는 단점, 선언 변수가 생기고 줄이 길어진다

# Non-Pythonic
words = ['python', 'is', 'fun']
sentence = ''
for word in words:
    sentence += word + ' '

 

파이서닉 한 join

# Pythonic
words = ['python', 'is', 'fun']
sentence = ' '.join(words)

print(sentence) # Output: python is fun

 

파이서닉 한 split(): 일반 케이스

sentence = "this is a  sentence with    irregular spacing"
words = sentence.split()
print(words)
# Output: ['this', 'is', 'a', 'sentence', 'with', 'irregular', 'spacing']

 

파이서닉 한 split(): delimiter 활용

csv_data = "apple,banana,cherry,date"
fruits = csv_data.split(',')
print(fruits)
# Output: ['apple', 'banana', 'cherry', 'date']

 

파이서닉 한 split(): split 수 제한, 제한을 넘어선 문장은 뭉쳐있다.

log_line = "INFO:user_login:user_id=123:details=successful"
parts = log_line.split(':', maxsplit=2)
print(parts)
# Output: ['INFO', 'user_login', 'user_id=123:details=successful']

 

파이서닉 한 split(): 정규표현식을 쓴 split

import re

text = "apple,banana;cherry orange"
items = re.split(r'[;, ]+', text)
print(items)
# Output: ['apple', 'banana', 'cherry', 'orange']

 

파이서닉 한 split(): 청크 단위로 Split 하기

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
chunk_size = 3

chunks = [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)]
print(chunks)
# Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

2. list comprehension, dictionary comprehension

컴프리헨션은 파이선에서 특정 데이터의 조합과 구성을 리스트와 딕셔너리로 효율적이고 보다 간결하게 만들기 위해 사용되는 기법이다.

 

파이써닉하지 못한 list 제작: for룹을 돌리는 문작을 작성하므로 코드가 길어진다

# Non-Pythonic
squares = []
for i in range(10):
    squares.append(i * i)

print(squares)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

 

파이서닉 한 list comprehension

# Pythonic with a condition
even_squares = [i * i for i in range(10) if i % 2 == 0]

print(even_squares)
# Output: [0, 4, 16, 36, 64]

 

파이써닉하지 못한 dictionary 제작: for룹을 돌리는 문작을 작성하므로 코드가 길어진다

# Non-Pythonic
numbers = [1, 2, 3, 4]
squares_dict = {}
for num in numbers:
    squares_dict[num] = num**2

print(squares_dict) # Output: {1: 1, 2: 4, 3: 9, 4: 16}

 

파이서닉 한 dictionary comprehension 제작

# Pythonic
numbers = [1, 2, 3, 4]
squares_dict = {num: num**2 for num in numbers}

print(squares_dict) # Output: {1: 1, 2: 4, 3: 9, 4: 16}

 


3. Iteration & Loop

List나 Dictionary와 같은 데이터 집합을 순회할 때 사용하는 기법

 

파이써닉하지 못한 list 순회: range에 len을 사용하여 코드를 길게 만드는 단점

# Non-Pythonic
fruits = ['apple', 'banana', 'cherry']
for i in range(len(fruits)):
    print(fruits[i])

 

파이서닉 한 list 순회

# Pythonic
fruits = ['apple', 'banana', 'cherry']
for fruit in fruits:
    print(fruit)

 

파이서닉 한 list 순회  Enumerate 활용

# Pythonic with index
for index, fruit in enumerate(fruits):
    print(f"Fruit at index {index} is {fruit}")

 

파이서닉 한 list 순회: 두개의 list를 동시 순차할 때 zip 활용

names = ["김승윤", "이서아", "박도윤"]
roles = ["개발자", "디자이너", "기획자"]

for index, (name, role) in enumerate(zip(names, roles), start=1):
    print(f"팀원 {index}: {name} ({role})")

4. Asterisk

함수 정의에서 *를 단독으로 사용하여, 그 뒤에 오는 인수들은 반드시 키워드로만 전달하도록 강제하는 기능입니다. 이는 함수의 가독성과 명확성을 높여줍니다.

 

파이써닉하지 못한 예시: 인수를 리스트로 받기

def calculate_sum(numbers_list):
    total = 0
    for num in numbers_list:
        total += num
    return total

# 함수를 호출할 때마다 리스트를 만들어야 함
print(calculate_sum([1, 2, 3]))
print(calculate_sum([10, 20, 30, 40, 50]))

 

파이서닉 한 예:  키워드 전용 인수 강제

def create_user_pythonic(name, age, *, is_admin, notify_by_email):
    print(f"{name}({age})님 생성. 관리자: {is_admin}, 이메일 알림: {notify_by_email}")

# create_user_pythonic("허진호", 29, True, False) -> TypeError 발생!

# 반드시 키워드로 전달해야 하므로 코드가 명확해짐
create_user_pythonic("허진호", 29, is_admin=True, notify_by_email=False)

6. Variadic Arguments

가변 인수란, 함수를 호출할 때 넘겨주는 인수의 개수가 정해져 있지 않고 유동적으로 변할 수 있다는 의미입니다.

 

방법1: Positional Arguments - 정해지지 않은 수의위치 인수들을 튜플(tuple)로 묶어서 받습니다.

 

파이써닉하지 못한 예시: 가변 인수를 처리하기 위해 리스트나 튜플을 단일 인수로 받는 방식입니다. 함수를 호출할 때마다 매번 리스트를 생성해야 해서 번거롭습니다.

def sum_numbers(numbers_list):
  # 함수는 리스트 하나만 인수로 받습니다.
  total = 0
  for num in numbers_list:
    total += num
  return total

# 호출할 때 리스트를 직접 만들어 전달해야 합니다.
result = sum_numbers([1, 2, 3, 4, 5])
print(f"합계: {result}")

 

파이서닉 한 예:  *args를 사용하면 함수를 호출할 때 인수를 쉼표로 구분하여 자유롭게 전달할 수 있습니다. 파이썬이 이 인수들을 알아서 하나의 튜플로 묶어줍니다.

def sum_numbers_pythonic(*args):
  # args는 (1, 2, 3, 4, 5) 형태의 튜플이 됩니다.
  print(f"받은 인수들: {args}")
  return sum(args)

# 인수를 원하는 만큼 자유롭게 전달할 수 있습니다.
result = sum_numbers_pythonic(1, 2, 3, 4, 5)
print(f"합계: {result}")

 

방법2: 키워드 인수 (Keyword Arguments) 처리 - 정해지지 않은 수의 키워드 인수들을 딕셔너리(dictionary)로 묶어서 받습니다.

 

파이써닉하지 못한 예시: 함수가 딕셔너리 하나를 인수로 받아 내부에서 처리하는 방식입니다. 호출할 때마다 딕셔너리를 만들어야 합니다.

def display_profile(profile_dict):
  # 함수는 딕셔너리 하나만 인수로 받습니다.
  print("--- 사용자 정보 ---")
  for key, value in profile_dict.items():
    print(f"{key}: {value}")

# 호출할 때 딕셔너리를 직접 만들어 전달해야 합니다.
user_info = {'name': '유수진', 'email': 'dev@example.com', 'level': 5}
display_profile(user_info)

 

파이서닉 한 예: **kwargs를 사용하면 이름=값 형태의 키워드 인수를 자유롭게 전달할 수 있습니다. 파이썬이 이 인수들을 알아서 하나의 딕셔너리로 묶어줍니다.

def display_profile_pythonic(**kwargs):
  # kwargs는 {'name': '유수진', ...} 형태의 딕셔너리가 됩니다.
  print(f"받은 인수들: {kwargs}")
  print("--- 사용자 정보 ---")
  for key, value in kwargs.items():
    print(f"{key}: {value}")

# 키워드 인수를 원하는 만큼 자유롭게 전달할 수 있습니다.
display_profile_pythonic(name='유수진', email='dev@example.com', level=5, status='active')

7. 컨테이너 타입 데이터 Unpacking

언패킹은 컨테이너 안의 요소들을 풀어서 여러 변수에 나누어 담거나, 함수의 인수로 전달하는 과정을 의미합니다.

 

파이써닉하지 못한 예시: 컨테이너의 각 요소를 인덱스나 키를 이용해 수동으로 하나씩 꺼내서 함수에 전달하는 방식입니다. 코드가 길어지고 컨테이너의 크기가 바뀌면 코드를 수정해야 합니다.

def create_rgb(red, green, blue):
  """RGB 값을 받아 색상 코드를 출력하는 함수"""
  print(f"R:{red}, G:{green}, B:{blue}")

# 리스트 언패킹 (못한 방식)
color_list = [255, 165, 0]  # 주황색
create_rgb(color_list[0], color_list[1], color_list[2])

# 딕셔너리 언패킹 (못한 방식)
color_dict = {'red': 65, 'green': 105, 'blue': 225} # 로얄 블루
create_rgb(red=color_dict['red'], green=color_dict['green'], blue=color_dict['blue'])

 

파이서닉 한 예:

def create_rgb_pythonic(red, green, blue):
  """RGB 값을 받아 색상 코드를 출력하는 함수"""
  print(f"R:{red}, G:{green}, B:{blue}")

# 리스트/튜플 언패킹
color_list = [255, 165, 0]
create_rgb_pythonic(*color_list) # * 하나를 붙이면 알아서 풀어서 전달

# 딕셔너리 언패킹
color_dict = {'red': 65, 'green': 105, 'blue': 225}
create_rgb_pythonic(**color_dict) # ** 두개를 붙이면 키워드 인수로 풀어서 전달
# (딕셔너리의 키가 함수의 매개변수 이름과 일치해야 합니다)

8. 파일과 리소스 다루기

 

파이써닉하지 못한 예시:

# Non-Pythonic
f = open('my_file.txt', 'w')
try:
    f.write('Hello, world!')
finally:
    f.close() # You must remember this!

 

파이서닉 한 예:

# Pythonic
with open('my_file.txt', 'w') as f:
    f.write('Hello, world!')

9. Dictionary 참조

 

파이써닉하지 못한 예시:

# Non-Pythonic
data = {'name': 'Alice', 'age': 30}

if 'city' in data:
    city = data['city']
else:
    city = 'Unknown'
print(city) # Output: Unknown

 

파이서닉 한 예:

# Pythonic
data = {'name': 'Alice', 'age': 30}
city = data.get('city', 'Unknown')

print(city) # Output: Unknown

10. 변수 스와핑

파이써닉하지 못한 예시:

# Non-Pythonic
a = 5
b = 10
temp = a
a = b
b = temp
print(f"a = {a}, b = {b}") # Output: a = 10, b = 5

 

파이서닉 한 예:

# Pythonic
a = 5
b = 10
a, b = b, a
print(f"a = {a}, b = {b}") # Output: a = 10, b = 5

11. Chained 비교문

파이써닉하지 못한 예시:

# Non-Pythonic
age = 25
if age > 18 and age < 30:
    print("You are in your 20s.")

 

파이서닉 한 예:

# Pythonic
age = 25
if 18 < age < 30:
    print("You are in your 20s.")

12. Ask for Forgiveness, Not Permission (EAFP)

파이써닉하지 못한 예시:

# Non-Pythonic (LBYL)
data = {'name': 'Alice', 'age': 30}

# We check if the key 'job' exists before using it
if 'job' in data and data['job'] is not None:
    print(data['job'])
else:
    print("No job specified.")

 

파이서닉 한 예:

# Pythonic (EAFP)
data = {'name': 'Alice', 'age': 30}

try:
    print(data['job'])
except KeyError:
    print("No job specified.")

13. 빈 리스트 비교

일반적으로 파이선은 None과 ""의 구분을 명확히 해야하는데요. 이 둘은 다른 두가지 형태를 비교하는 구문이기 때문입니다.

 

  • variable is None: 변수가 '값이 없음'이라는 특별한 상태인지 확인합니다.
  • variable == "": 변수가 '비어있는 문자열'이라는 값을 가지고 있는지 확인합니다.

 

None은 파이썬에서 "값이 없음"을 나타내는 유일무이한 싱글턴(singleton) 객체입니다. 프로그램 전체에서 None은 단 하나만 존재합니다.

 

is 연산자는 두 변수가 같은 메모리 주소를 가리키는 동일한 객체인지를 확인하는 식별성(identity) 비교입니다. None은 항상 유일한 객체이므로, None인지 확인할 때는 is를 사용하는 것이 가장 정확하고 빠릅니다.

 

파이써닉하지 못한 예시:

my_var = None
if my_var == None: # 작동은 하지만, Pythonic하지 않음
    print("변수가 None과 '값이' 같습니다.")

 

파이서닉 한 예:

my_var = None
if my_var is None: # None을 확인할 때는 반드시 is를 사용합니다.
    print("변수가 None 객체 '자체'입니다.")

 

비어있는 객체 확인하기

 

파이써닉하지 못한 예시:

my_list = []
if len(my_list) == 0:
    print("리스트가 비어있습니다. (len() 사용)")

 

파이서닉 한 예:

my_list = []
if not my_list:
    print("리스트가 비어있습니다. (Pythonic 방식)")

 

파이썬에서는 비어있는 컨테이너 타입을 암시적으로 False로 취급합니다. 이를 "Falsy"하다고 표현합니다. 반대로 내용이 하나라도 있으면 "Truthy"하다고 하여 True로 취급합니다.

 

이 원리를 이용하면 if not my_list: 라는 코드는 "my_list가 비어있다면(False라면)" 이라는 의미가 되어, len()을 호출할 필요 없이 훨씬 간결하고 직관적인 코드가 완성됩니다.

 

다양한 "Falsy" 값들: 이 규칙은 다른 여러 값에도 동일하게 적용됩니다.

  • [] (빈 리스트)
  • () (빈 튜플)
  • {} (빈 딕셔너리)
  • "" (빈 문자열)
  • 0 (숫자 0)
  • None

따라서 비어있는 문자열을 확인할 때도 if not my_string:을 사용할 수 있습니다.


14. 메모리 최적화를 위한 Generator Expression

 

파이써닉하지 못한 예시:

# Non-Pythonic (for large data)
# This creates a list with one million numbers in memory
total = sum([i * i for i in range(1000000)])
print(total)

 

파이서닉 한 예:

# Pythonic
# This creates a generator object that yields values on demand
total = sum(i * i for i in range(1000000))
print(total)

15. 함수에 list, dictionary 값의 매개변수를 전달 할 때

파이써닉하지 못한 예시:

# Non-Pythonic
def move_character(x, y, z):
    print(f"Moving to coordinates ({x}, {y}, {z})")

# Positional arguments from a list
coords_list = [10, 20, 5]
move_character(coords_list[0], coords_list[1], coords_list[2])

# Keyword arguments from a dictionary
coords_dict = {'x': 10, 'y': 20, 'z': 5}
move_character(x=coords_dict['x'], y=coords_dict['y'], z=coords_dict['z'])

 

파이서닉 한 예:

# Pythonic
def move_character(x, y, z):
    print(f"Moving to coordinates ({x}, {y}, {z})")

# Unpacking a list for positional arguments
coords_list = [10, 20, 5]
move_character(*coords_list)

# Unpacking a dictionary for keyword arguments
coords_dict = {'x': 10, 'y': 20, 'z': 5}
move_character(**coords_dict)

 


16. Mutable 기본 매개변수의 위험성 제어

 

파이써닉하지 못한 예시:

# Non-Pythonic (Dangerous!)
def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [1, 2]  <-- Unexpected!
print(add_item(3))  # Output: [1, 2, 3] <-- The list persists across calls.

 

파이서닉 한 예:

# Pythonic
def add_item(item, my_list=None):
    if my_list is None:
        my_list = []  # Create a new list only when needed
    my_list.append(item)
    return my_list

print(add_item(1))  # Output: [1]
print(add_item(2))  # Output: [2]
print(add_item(3, [10, 20])) # Output: [10, 20, 3]

 


17. 사용하지 않는 변수는 _으로 처리하기

파이써닉하지 못한 예시:

# Non-Pythonic
person = ('John', 'Doe', 42)
first, last, dummy = person
print(f"{first} {last}")

for unused in range(5):
    print("Hello!")

 

파이서닉 한 예:

# Pythonic
person = ('John', 'Doe', 42)
first, last, _ = person # We don't need the age
print(f"{first} {last}")

for _ in range(5): # We don't need the loop counter
    print("Hello!")

 


18. 그룹화 할 때 collections.defaultdict 사용하기

파이써닉하지 못한 예시:

# Non-Pythonic
fruits = [('apple', 'red'), ('banana', 'yellow'), ('apple', 'green'), ('cherry', 'red')]
grouped = {}

for fruit, color in fruits:
    if color not in grouped:
        grouped[color] = [] # Initialize the list for a new color
    grouped[color].append(fruit)

print(grouped)
# Output: {'red': ['apple', 'cherry'], 'yellow': ['banana'], 'green': ['apple']}

 

파이서닉 한 예:

# Pythonic
from collections import defaultdict

fruits = [('apple', 'red'), ('banana', 'yellow'), ('apple', 'green'), ('cherry', 'red')]
grouped = defaultdict(list) # The factory for new keys is 'list'

for fruit, color in fruits:
    grouped[color].append(fruit) # No check needed!

print(grouped)
# Output: defaultdict(<class 'list'>, {'red': ['apple', 'cherry'], 'yellow': ['banana'], 'green': ['apple']})

 


19. 조합을 구할 때 itertools 활용하기

파이써닉하지 못한 예시:

# Non-Pythonic
team_a = ['Alice', 'Bob']
team_b = ['Charlie', 'David']
pairings = []

for p1 in team_a:
    for p2 in team_b:
        pairings.append((p1, p2))
        
print(pairings)
# Output: [('Alice', 'Charlie'), ('Alice', 'David'), ('Bob', 'Charlie'), ('Bob', 'David')]

 

파이서닉 한 예:

# Pythonic
import itertools

team_a = ['Alice', 'Bob']
team_b = ['Charlie', 'David']

# Get the Cartesian product of the two lists
pairings = list(itertools.product(team_a, team_b))

print(pairings)
# Output: [('Alice', 'Charlie'), ('Alice', 'David'), ('Bob', 'Charlie'), ('Bob', 'David')]

 


20. Decorator

서로를 계속 호출하는 상호 재귀(Mutually Recursive) 함수들은 자칫하면 무한 루프에 빠지거나 호출 흐름을 추적하기 어려워 관리가 힘들어질 수 있습니다.

 

이때 파이썬의 @(Annotation)을 활용하면, 재귀의 깊이를 제어하거나 호출 상태를 기록하는 관리 로직을 실제 함수 비즈니스 로직과 분리하여 코드를 매우 깔끔하고 용이하게 만들 수 있습니다.

 

파이써닉하지 못한 예시:

  • 로직 중복: 깊이를 체크하고 출력하는 코드가 두 함수에 똑같이 반복됩니다. (DRY 원칙 위배)
  • 낮은 가독성: 함수의 핵심 로직(ping이 pong을 부르는 것)과 관리 로직(깊이 체크)이 섞여 있어 코드를 이해하기 어렵습니다.
# 못한 예시: 깊이를 인자로 넘기며 로직 중복 발생

MAX_DEPTH = 5

def ping(depth=0):
    print(f"{'  ' * depth} -> ping 호출됨 (깊이: {depth})")
    
    # 관리 로직: 깊이 체크
    if depth >= MAX_DEPTH:
        print(f"{'  ' * depth} 최대 깊이에 도달하여 중단합니다.")
        return
        
    # 비즈니스 로직: pong 호출
    pong(depth + 1)
    print(f"{'  ' * depth} <- ping 종료됨")


def pong(depth=0):
    print(f"{'  ' * depth} -> pong 호출됨 (깊이: {depth})")
    
    # 관리 로직: 깊이 체크 (ping과 동일한 코드가 중복됨)
    if depth >= MAX_DEPTH:
        print(f"{'  ' * depth} 최대 깊이에 도달하여 중단합니다.")
        return
        
    # 비즈니스 로직: ping 호출
    ping(depth + 1)
    print(f"{'  ' * depth} <- pong 종료됨")

print("--- 못한 예시 실행 ---")
ping()

 

파이서닉 한 예:

# Pythonic한 예시: 관리 로직을 데코레이터로 분리

import functools

def recursion_manager(func):
    """상호 재귀 호출을 관리하는 데코레이터"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 깊이를 wrapper 함수의 속성으로 관리하여 호출간에 공유
        if not hasattr(wrapper, "depth"):
            wrapper.depth = 0
        
        print(f"{'  ' * wrapper.depth} -> {func.__name__} 호출됨 (깊이: {wrapper.depth})")
        
        # 관리 로직: 깊이 체크
        if wrapper.depth >= 5:
            print(f"{'  ' * wrapper.depth} 최대 깊이에 도달하여 중단합니다.")
            return

        wrapper.depth += 1
        # 원래 함수(ping 또는 pong)의 비즈니스 로직 실행
        result = func(*args, **kwargs)
        wrapper.depth -= 1
        
        print(f"{'  ' * wrapper.depth} <- {func.__name__} 종료됨")
        return result
    return wrapper

# @데코레이터만 붙여주면 관리 기능이 자동으로 적용됨
@recursion_manager
def ping():
    # 비즈니스 로직만 남음
    pong()

@recursion_manager
def pong():
    # 비즈니스 로직만 남음
    ping()

print("\n--- Pythonic한 예시 실행 ---")
ping()

 

 

장점:

  • 관심사의 분리(Separation of Concerns): 함수의 핵심 로직과 관리 로직이 완벽하게 분리되었습니다.
  • 재사용성: recursion_manager 데코레이터는 다른 재귀 함수에도 그대로 가져다 쓸 수 있습니다.
  • 유지보수 용이성: 최대 깊이를 10으로 바꾸고 싶다면, 데코레이터 내부의 5라는 숫자 하나만 수정하면 됩니다.

21. collections.namedtuple

 

튜플 문서화를 한 collections.namedtuple

 

파이써닉하지 못한 예시:

# Non-Pythonic
# Using a plain tuple
color = (255, 165, 0)
print(f"Red component is {color[0]}") # What is index 0?

# Using a dictionary
color_dict = {'red': 255, 'green': 165, 'blue': 0}
# This is mutable and less lightweight

 

파이서닉 한 예:

# Pythonic
from collections import namedtuple

# Define the structure of our named tuple
Color = namedtuple('Color', ['red', 'green', 'blue'])

# Create an instance
orange = Color(red=255, green=165, blue=0)

print(f"Red component is {orange.red}") # Clean and readable
print(f"Green component is {orange[1]}") # Still accessible by index

 


22. any() 와 all() 활용

파이써닉하지 못한 예시:

# Non-Pythonic
widgets = [
    {'name': 'widget1', 'in_stock': True},
    {'name': 'widget2', 'in_stock': False},
    {'name': 'widget3', 'in_stock': True}
]

# Check if at least one widget is out of stock
any_out_of_stock = False
for widget in widgets:
    if not widget['in_stock']:
        any_out_of_stock = True
        break # Stop as soon as we find one

print(f"Is any widget out of stock? {any_out_of_stock}")

 

파이서닉 한 예:

# Pythonic
widgets = [
    {'name': 'widget1', 'in_stock': True},
    {'name': 'widget2', 'in_stock': False},
    {'name': 'widget3', 'in_stock': True}
]

# Reads like English: "if not widget['in_stock'] for any widget"
is_any_out_of_stock = any(not widget['in_stock'] for widget in widgets)

# Reads like English: "if widget['in_stock'] for all widgets"
are_all_in_stock = all(widget['in_stock'] for widget in widgets)

print(f"Is any widget out of stock? {is_any_out_of_stock}") # True
print(f"Are all widgets in stock? {are_all_in_stock}")     # False