Python async defとは?awaitとの関係・使い方・async def main()の書き方をわかりやすく解説
async defは「非同期関数(コルーチン関数)」を定義する書き方で、awaitはその中で「待ち時間のある処理を待ち、待っている間はほかの処理に順番を譲る」ためのキーワードです。async defの関数は呼んだだけでは動かず、asyncio.run()かawaitで実行します。
この記事では、async def main()の基本形、awaitを並べても速くならない理由とgather()での同時実行、awaitの書き忘れで出るエラーや警告、FastAPIでasync defとdefのどちらを使うかまで、実行結果つきで解説します。実行結果はPython 3.12.15で確認しました(2026年10月18日確認)。
async defとawaitを一言でいうと
| キーワード | 役割 | 書ける場所 |
|---|---|---|
async def |
非同期関数を定義する。呼ぶとコルーチンが返る | どこでも |
await |
コルーチンなどの結果を待つ。待つ間はほかの処理に譲る | async defの中だけ |
asyncio.run() |
いちばん外側で非同期処理を始める入口 | 通常の関数・トップレベル |
非同期処理とは、通信やファイル読み込みのような「待ち時間」の間に、ほかの処理を進めるやり方です。1つのスレッドで、待ち時間を無駄にしないように処理を切り替えます。非同期処理そのものの考え方は「非同期処理とは?仕組みから実装まで初心者にもわかる完全解説」で詳しく解説しています。
async defで定義した関数は、呼んでも実行されない
import asyncio
async def hello():
print("実行された")
return 42
c = hello()
print(type(c).__name__)
c.close()
print(asyncio.run(hello()))
coroutine
実行された
42
hello()と呼んだ時点では「実行された」が表示されていません。async defの関数を呼ぶと、中身を実行する代わりにコルーチンというオブジェクトが返ります。実際に動かすには、asyncio.run()に渡すか、別のasync defの中でawaitします(上のc.close()は、使わなかったコルーチンの警告を出さないための後始末です)。
基本形:async def main() と asyncio.run(main())
非同期のプログラムは、ほぼ次の形で書きます。
import asyncio
async def main():
print("main開始")
asyncio.run(main())
main開始
処理の本体をasync def main()にまとめ、プログラムの最後でasyncio.run(main())を1回だけ呼びます。asyncio.run()が非同期処理を動かす仕組み(イベントループ)を用意し、main()が終わると片付けまで行います。asyncio.run()の引数や戻り値、よくあるエラーは「Python asyncio.run()の使い方|main()の書き方・戻り値・running event loopエラーの直し方」で解説しています。
asyncio.run()を付け忘れてmain()とだけ書くと、何も実行されずに警告が出ます。
RuntimeWarning: coroutine 'main' was never awaited
awaitを順番に並べても速くならない
import asyncio
import time
async def work(name, sec):
await asyncio.sleep( sec)
return f"{name}完了"
async def main():
s = time.perf_counter()
a = await work("A", 1)
b = await work("B", 1)
print(a, b, f"順番にawait: {time.perf_counter() - s:.1f}秒")
s = time.perf_counter()
a, b = await asyncio.gather(work("A", 1), work("B", 1))
print(a, b, f"gatherで同時に: {time.perf_counter() - s:.1f}秒")
asyncio.run(main())
A完了 B完了 順番にawait: 2.0秒
A完了 B完了 gatherで同時に: 1.0秒
awaitには「結果が出るまで待つ」と「待っている間、ほかの処理に順番を譲る」の2つの意味があります。ところが1つずつawaitすると、Aが終わるまでBは始まらないので、合計2秒かかります。同時に進めたい処理はasyncio.gather()にまとめて渡すと、待ち時間が重なって1秒で終わります。gather()の詳しい使い方は「Python asyncio.gather()とは?複数の処理を同時に実行する方法」をご覧ください。
awaitを忘れたときのエラーと警告
値として使うとTypeError
import asyncio
async def get():
return 10
async def main():
v = get() # await を忘れた
print(v > 5)
asyncio.run(main())
TypeError: '>' not supported between instances of 'coroutine' and 'int'
sys:1: RuntimeWarning: coroutine 'get' was never awaited
変数vに入っているのは数値ではなくコルーチンです。「なぜか数値として使えない」「中身が<coroutine object ...>になっている」という症状は、ほぼawaitの書き忘れです。
戻り値を使わないと警告だけ
import asyncio
async def get():
return 10
async def main():
get() # await を忘れた(戻り値も使っていない)
print("終了")
asyncio.run(main())
RuntimeWarning: coroutine 'get' was never awaited
終了
エラーで止まらず、処理がまるごと実行されないまま進みます。coroutine '...' was never awaitedが出たら、その関数の前にawaitを付け忘れていると考えてください。
awaitはasync defの中でしか使えない
import asyncio
def g():
await asyncio.sleep( 0)
SyntaxError: 'await' outside async function
普通のdefの中でawaitを書くと、実行前に文法エラーになります。awaitを使う関数はasync defにし、その関数を呼ぶ側もasync defにしてawaitする、という形で上へ伝わっていきます。いちばん上はasyncio.run()で始めます。
awaitできるのは、コルーチン、asyncio.create_task()で作ったTask、Futureなどの「待てるオブジェクト」だけです。普通の値をawaitするとTypeError: object int can't be used in 'await' expressionになります。
async with と async for
import asyncio
class Conn:
async def __aenter__(self):
print("open")
return self
async def __aexit__(self, *args):
print("close")
return False
async def agen():
for i in range(3):
await asyncio.sleep( 0.01)
yield i
async def main():
async with Conn() as c:
print("処理中")
print([x async for x in agen()])
asyncio.run(main())
open
処理中
close
[0, 1, 2]
async withは、接続を開く・閉じるといった前後処理にも待ち時間があるときに使います(__aenter__と__aexit__が非同期版の特殊メソッドです)。yieldを含むasync defは非同期ジェネレータになり、async forで1件ずつ取り出せます。
async defの中にtime.sleep()など同期処理を混ぜない
import asyncio
import time
async def blocking():
time.sleep( 1) # 同期のsleep
return "完了"
async def nonblocking():
await asyncio.to_thread(time.sleep, 1)
return "完了"
async def main():
s = time.perf_counter()
await asyncio.gather(blocking(), blocking())
print(f"time.sleepを混ぜた場合: {time.perf_counter() - s:.1f}秒")
s = time.perf_counter()
await asyncio.gather(nonblocking(), nonblocking())
print(f"to_threadに逃がした場合: {time.perf_counter() - s:.1f}秒")
asyncio.run(main())
time.sleepを混ぜた場合: 2.0秒
to_threadに逃がした場合: 1.0秒
time.sleep()やrequests.get()のような同期処理は、待っている間も順番を譲らないので、gather()しても同時に進みません。async defの中では非同期に対応した関数を使い、どうしても同期の処理を呼ぶときはasyncio.to_thread()で別スレッドに逃がします。
| やりたいこと | async def の中で使うもの |
|---|---|
| 一定時間待つ | await asyncio.sleep() |
| HTTP通信 | aiohttpやhttpxの非同期API |
| 同期のライブラリしかない処理 | await asyncio.to_thread(関数, 引数) |
| 複数の処理を同時に進める | asyncio.gather()、asyncio.create_task() |
to_thread()については「Python asyncio.to_thread()とは?同期処理を非同期で動かす方法」で解説しています。
FastAPIではasync defとdefのどちらを使う?
「async def」で調べる人の多くが出会うのが、WebフレームワークFastAPIの公式ドキュメントにある使い分けです(2026年10月18日確認)。要点は次のとおりです。
- 使っているライブラリが
awaitで呼ぶよう案内している(非同期対応)なら、処理の関数をasync defで書く - データベースなど外部とやり取りするライブラリが
awaitに対応していないなら、普通のdefで書く。FastAPIはdefの関数を別のスレッドプールで実行するので、サーバー全体は止まらない - 外部とのやり取りがなく待ち時間がない処理なら、
async defでよい
上の「同期処理を混ぜない」と同じ理由で、async defの中で非対応のライブラリを呼ぶのがいちばん危険です。迷ったら、中でawaitするものがあるかどうかで決めると間違えにくくなります。
よくある質問
Q. async defとdefの違いは?
defの関数は呼ぶとすぐ実行されて値を返します。async defの関数は呼ぶとコルーチンが返るだけで、awaitするかasyncio.run()に渡したときに実行されます。またawaitはasync defの中でしか書けません。
Q. async defの関数を普通の関数から呼ぶには?
プログラムのいちばん外側でasyncio.run(関数())と呼びます。すでに非同期処理が動いている中(async defの中)ではasyncio.run()ではなくawaitを使います。
Q. coroutine was never awaitedという警告の意味は?
async defの関数を呼んだのにawaitしていないため、中身が一度も実行されなかったという意味です。呼び出しの前にawaitを付けるか、asyncio.create_task()やasyncio.gather()に渡してください。
Q. awaitを付けたのに処理が速くならないのはなぜ?
1つずつawaitすると前の処理が終わるまで次が始まらないためです。同時に進めたい処理はasyncio.gather()にまとめます。またtime.sleep()のような同期処理が混ざっていると、gather()しても同時には進みません。
Q. FastAPIではasync defとdefのどちらを使えばいい?
FastAPIの公式ドキュメントでは、awaitで呼ぶ非同期対応のライブラリを使うならasync def、awaitに対応していないライブラリを使うなら普通のdefを勧めています。defの関数はFastAPIが別スレッドで実行します。
async defとawaitのまとめ
async defの関数は呼んでも実行されず、コルーチンが返る- 基本形は
async def main()と、最後に1回だけasyncio.run(main()) awaitは「待つ」と「待つ間ほかに譲る」。順番に並べても速くならないので、同時に進めるならgather()coroutine ... was never awaitedはawaitの書き忘れawaitはasync defの中だけ。外で書くとSyntaxErrortime.sleep()など同期処理を混ぜない。必要ならasyncio.to_thread()
非同期処理は、読んだだけでは「どこで待っているのか」がつかみにくい分野です。テックジムでは、自分のペースで課題を進めながら、つまずいたところをその場でトレーナーに質問できます。入会金33,000円+月額22,000円で通い放題(学生は月額16,500円)です。まずは無料講座で雰囲気を確かめてみてください。





